mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-10 19:06:34 +03:00
Implement expandResponses option
This commit is contained in:
parent
e85718225d
commit
f52a05a2b5
|
@ -6,6 +6,7 @@ import { AppContainer } from 'react-hot-loader';
|
|||
import { Redoc, RedocProps } from '../../src/components/Redoc/Redoc';
|
||||
import { AppStore } from '../../src/services/AppStore';
|
||||
import { loadAndBundleSpec } from '../../src/utils/loadAndBundleSpec';
|
||||
import { RedocRawOptions } from '../../src/services/RedocNormalizedOptions';
|
||||
|
||||
const renderRoot = (Component: typeof Redoc, props: RedocProps) =>
|
||||
render(
|
||||
|
@ -23,12 +24,12 @@ const swagger = window.location.search.indexOf('swagger') > -1; //compatibility
|
|||
const specUrl = swagger ? 'swagger.yaml' : big ? 'big-openapi.json' : 'openapi.yaml';
|
||||
|
||||
let store;
|
||||
const options = {};
|
||||
const options: RedocRawOptions = { expandResponses: 'all' };
|
||||
|
||||
async function init() {
|
||||
const spec = await loadAndBundleSpec(specUrl);
|
||||
store = new AppStore(spec, specUrl);
|
||||
renderRoot(Redoc, { store: store, options });
|
||||
store = new AppStore(spec, specUrl, options);
|
||||
renderRoot(Redoc, { store: store });
|
||||
}
|
||||
|
||||
init();
|
||||
|
@ -43,7 +44,7 @@ if (module.hot) {
|
|||
store = AppStore.fromJS(state);
|
||||
}
|
||||
|
||||
renderRoot(Redoc, { store: store, options });
|
||||
renderRoot(Redoc, { store: store });
|
||||
};
|
||||
|
||||
module.hot.accept(['../../src/components/Redoc/Redoc'], reload());
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import * as React from 'react';
|
||||
import * as PropTypes from 'prop-types';
|
||||
|
||||
import { RedocNormalizedOptions, RedocRawOptions } from '../services/RedocNormalizedOptions';
|
||||
import { RedocNormalizedOptions } from '../services/RedocNormalizedOptions';
|
||||
|
||||
export interface OptionsProviderProps {
|
||||
options: RedocRawOptions;
|
||||
options: RedocNormalizedOptions;
|
||||
}
|
||||
|
||||
export class OptionsProvider extends React.Component<OptionsProviderProps> {
|
||||
|
@ -14,7 +14,7 @@ export class OptionsProvider extends React.Component<OptionsProviderProps> {
|
|||
|
||||
getChildContext() {
|
||||
return {
|
||||
redocOptions: new RedocNormalizedOptions(this.props.options),
|
||||
redocOptions: this.props.options,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,19 +11,14 @@ import { ContentItems } from '../ContentItems/ContentItems';
|
|||
import { AppStore } from '../../services';
|
||||
import { OptionsProvider } from '../OptionsProvider';
|
||||
import { StickySidebar } from '../StickySidebar/StickySidebar';
|
||||
import { RedocRawOptions } from '../../services/RedocNormalizedOptions';
|
||||
|
||||
import defaultTheme from '../../theme';
|
||||
|
||||
export interface RedocProps {
|
||||
store: AppStore;
|
||||
options?: RedocRawOptions;
|
||||
}
|
||||
|
||||
export class Redoc extends React.Component<RedocProps> {
|
||||
static propTypes = {
|
||||
store: PropTypes.instanceOf(AppStore).isRequired,
|
||||
options: PropTypes.object,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -31,9 +26,9 @@ export class Redoc extends React.Component<RedocProps> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { store: { spec, menu }, options = {} } = this.props;
|
||||
const { store: { spec, menu, options } } = this.props;
|
||||
return (
|
||||
<ThemeProvider theme={{ ...options.theme, ...defaultTheme }}>
|
||||
<ThemeProvider theme={options.theme}>
|
||||
<OptionsProvider options={options}>
|
||||
<RedocWrap className="redoc-wrap">
|
||||
<StickySidebar className="menu-content">
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { ThemeInterface } from '../theme';
|
||||
|
||||
import { LoadingWrap } from './LoadingWrap/LoadingWrap';
|
||||
import { StoreProvider } from './StoreProvider';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
import { Redoc } from './Redoc/Redoc';
|
||||
import { RedocRawOptions } from '../services/RedocNormalizedOptions';
|
||||
|
||||
export interface RedocStandaloneProps {
|
||||
spec?: object;
|
||||
specUrl?: string;
|
||||
options?: {
|
||||
theme?: ThemeInterface;
|
||||
};
|
||||
options?: RedocRawOptions;
|
||||
}
|
||||
|
||||
export class RedocStandalone extends React.Component<RedocStandaloneProps> {
|
||||
|
@ -41,10 +38,10 @@ export class RedocStandalone extends React.Component<RedocStandaloneProps> {
|
|||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<StoreProvider spec={spec} specUrl={specUrl}>
|
||||
<StoreProvider spec={spec} specUrl={specUrl} options={options}>
|
||||
{({ loading, store }) => (
|
||||
<LoadingWrap loading={loading}>
|
||||
<Redoc store={store} options={options} />
|
||||
<Redoc store={store} />
|
||||
</LoadingWrap>
|
||||
)}
|
||||
</StoreProvider>
|
||||
|
|
|
@ -34,23 +34,24 @@ export class ResponseView extends React.Component<{ response: ResponseModel }> {
|
|||
code={code}
|
||||
opened={expanded}
|
||||
/>
|
||||
{expanded && (
|
||||
<ResponseDetailsWrap>
|
||||
<ResponseHeaders headers={headers} />
|
||||
<MediaTypesSwitch
|
||||
content={content!}
|
||||
renderDropdown={props => (
|
||||
<UnderlinedHeader key="header">
|
||||
Response Schema: <DropdownOrLabel {...props} />
|
||||
</UnderlinedHeader>
|
||||
)}
|
||||
>
|
||||
{({ schema }) => {
|
||||
return <Schema skipWriteOnly={true} key="schema" schema={schema} />;
|
||||
}}
|
||||
</MediaTypesSwitch>
|
||||
</ResponseDetailsWrap>
|
||||
)}
|
||||
{expanded &&
|
||||
!empty && (
|
||||
<ResponseDetailsWrap>
|
||||
<ResponseHeaders headers={headers} />
|
||||
<MediaTypesSwitch
|
||||
content={content!}
|
||||
renderDropdown={props => (
|
||||
<UnderlinedHeader key="header">
|
||||
Response Schema: <DropdownOrLabel {...props} />
|
||||
</UnderlinedHeader>
|
||||
)}
|
||||
>
|
||||
{({ schema }) => {
|
||||
return <Schema skipWriteOnly={true} key="schema" schema={schema} />;
|
||||
}}
|
||||
</MediaTypesSwitch>
|
||||
</ResponseDetailsWrap>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,9 +11,11 @@ const ResponsesHeader = styled.h3`
|
|||
font-weight: normal;
|
||||
`;
|
||||
|
||||
export class ResponsesList extends React.PureComponent<{
|
||||
export interface ResponseListProps {
|
||||
responses: ResponseModel[];
|
||||
}> {
|
||||
}
|
||||
|
||||
export class ResponsesList extends React.PureComponent<ResponseListProps> {
|
||||
render() {
|
||||
const { responses } = this.props;
|
||||
|
||||
|
|
|
@ -2,12 +2,15 @@ import { Component } from 'react';
|
|||
|
||||
import { AppStore } from '../services/';
|
||||
import { loadAndBundleSpec } from '../utils';
|
||||
import { RedocRawOptions } from '../services/RedocNormalizedOptions';
|
||||
|
||||
interface SpecProps {
|
||||
specUrl?: string;
|
||||
spec?: object;
|
||||
store?: AppStore;
|
||||
|
||||
options?: RedocRawOptions;
|
||||
|
||||
children?: any;
|
||||
}
|
||||
|
||||
|
@ -33,7 +36,7 @@ export class StoreProvider extends Component<SpecProps, SpecState> {
|
|||
}
|
||||
|
||||
async load() {
|
||||
let { specUrl, spec } = this.props;
|
||||
let { specUrl, spec, options } = this.props;
|
||||
|
||||
this.setState({
|
||||
loading: true,
|
||||
|
@ -43,7 +46,7 @@ export class StoreProvider extends Component<SpecProps, SpecState> {
|
|||
const resolvedSpec = await loadAndBundleSpec(spec || specUrl!);
|
||||
this.setState({
|
||||
loading: false,
|
||||
store: new AppStore(resolvedSpec, specUrl),
|
||||
store: new AppStore(resolvedSpec, specUrl, options),
|
||||
});
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
|
|
|
@ -3,6 +3,7 @@ import { SpecStore } from './models';
|
|||
import { MenuStore } from './MenuStore';
|
||||
import { ScrollService } from './ScrollService';
|
||||
import { loadAndBundleSpec } from '../utils/loadAndBundleSpec';
|
||||
import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOptions';
|
||||
|
||||
type StoreData = {
|
||||
menu: {
|
||||
|
@ -12,22 +13,31 @@ type StoreData = {
|
|||
url: string;
|
||||
data: any;
|
||||
};
|
||||
options: RedocRawOptions;
|
||||
};
|
||||
|
||||
export async function createStore(spec: object, specUrl: string) {
|
||||
export async function createStore(
|
||||
spec: object,
|
||||
specUrl: string | undefined,
|
||||
options: RedocRawOptions = {},
|
||||
) {
|
||||
const resolvedSpec = await loadAndBundleSpec(spec || specUrl);
|
||||
return new AppStore(resolvedSpec, specUrl);
|
||||
return new AppStore(resolvedSpec, specUrl, options);
|
||||
}
|
||||
|
||||
export class AppStore {
|
||||
menu: MenuStore;
|
||||
spec: SpecStore;
|
||||
rawOptions: RedocRawOptions;
|
||||
options: RedocNormalizedOptions;
|
||||
|
||||
private scroll: ScrollService;
|
||||
|
||||
constructor(spec: OpenAPISpec, specUrl?: string) {
|
||||
constructor(spec: OpenAPISpec, specUrl?: string, options: RedocRawOptions = {}) {
|
||||
this.rawOptions = options;
|
||||
this.options = new RedocNormalizedOptions(options);
|
||||
this.scroll = new ScrollService();
|
||||
this.spec = new SpecStore(spec, specUrl);
|
||||
this.spec = new SpecStore(spec, specUrl, this.options);
|
||||
this.menu = new MenuStore(this.spec, this.scroll);
|
||||
}
|
||||
|
||||
|
@ -50,6 +60,7 @@ export class AppStore {
|
|||
url: this.spec.parser.specUrl,
|
||||
data: this.spec.parser.spec,
|
||||
},
|
||||
options: this.rawOptions,
|
||||
};
|
||||
}
|
||||
/**
|
||||
|
@ -58,7 +69,7 @@ export class AppStore {
|
|||
*/
|
||||
// TODO:
|
||||
static fromJS(state: StoreData): AppStore {
|
||||
const inst = new AppStore(state.spec.data, state.spec.url);
|
||||
const inst = new AppStore(state.spec.data, state.spec.url, state.options);
|
||||
inst.menu.activeItemIdx = state.menu.activeItemIdx || 0;
|
||||
inst.menu.activate(inst.menu.flatItems[inst.menu.activeItemIdx]);
|
||||
return inst;
|
||||
|
|
|
@ -3,6 +3,7 @@ import { GroupModel, OperationModel } from './models';
|
|||
import { JsonPointer, isOperationName } from '../utils';
|
||||
import { OpenAPIOperation, OpenAPIParameter, OpenAPISpec, OpenAPITag, Referenced } from '../types';
|
||||
import { MarkdownRenderer } from './MarkdownRenderer';
|
||||
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
||||
|
||||
export type TagInfo = OpenAPITag & {
|
||||
operations: ExtendedOpenAPIOperation[];
|
||||
|
@ -29,16 +30,21 @@ export class MenuBuilder {
|
|||
/**
|
||||
* Builds page content structure based on tags
|
||||
*/
|
||||
static buildStructure(parser: OpenAPIParser): ContentItemModel[] {
|
||||
static buildStructure(
|
||||
parser: OpenAPIParser,
|
||||
options: RedocNormalizedOptions,
|
||||
): ContentItemModel[] {
|
||||
const spec = parser.spec;
|
||||
|
||||
const items: ContentItemModel[] = [];
|
||||
const tagsMap = MenuBuilder.getTagsWithOperations(spec);
|
||||
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || ''));
|
||||
if (spec['x-tagGroups']) {
|
||||
items.push(...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap));
|
||||
items.push(
|
||||
...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options),
|
||||
);
|
||||
} else {
|
||||
items.push(...MenuBuilder.getTagsItems(parser, tagsMap));
|
||||
items.push(...MenuBuilder.getTagsItems(parser, tagsMap, undefined, undefined, options));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
@ -62,12 +68,13 @@ export class MenuBuilder {
|
|||
parent: GroupModel | undefined,
|
||||
groups: TagGroup[],
|
||||
tags: TagsInfoMap,
|
||||
options: RedocNormalizedOptions,
|
||||
): GroupModel[] {
|
||||
let res: GroupModel[] = [];
|
||||
for (let group of groups) {
|
||||
let item = new GroupModel('group', group, parent);
|
||||
item.depth = GROUP_DEPTH;
|
||||
item.items = MenuBuilder.getTagsItems(parser, tags, item, group);
|
||||
item.items = MenuBuilder.getTagsItems(parser, tags, item, group, options);
|
||||
res.push(item);
|
||||
}
|
||||
// TODO checkAllTagsUsedInGroups
|
||||
|
@ -83,8 +90,9 @@ export class MenuBuilder {
|
|||
static getTagsItems(
|
||||
parser: OpenAPIParser,
|
||||
tagsMap: TagsInfoMap,
|
||||
parent?: GroupModel,
|
||||
group?: TagGroup,
|
||||
parent: GroupModel | undefined,
|
||||
group: TagGroup | undefined,
|
||||
options: RedocNormalizedOptions,
|
||||
): ContentItemModel[] {
|
||||
let tagNames;
|
||||
|
||||
|
@ -108,11 +116,11 @@ export class MenuBuilder {
|
|||
if (!tag) continue;
|
||||
let item = new GroupModel('tag', tag, parent);
|
||||
item.depth = GROUP_DEPTH + 1;
|
||||
item.items = this.getOperationsItems(parser, item, tag, item.depth + 1);
|
||||
item.items = this.getOperationsItems(parser, item, tag, item.depth + 1, options);
|
||||
|
||||
// don't put empty tag into content, instead put its operations
|
||||
if (tag.name === '') {
|
||||
let items = this.getOperationsItems(parser, undefined, tag, item.depth);
|
||||
let items = this.getOperationsItems(parser, undefined, tag, item.depth, options);
|
||||
res.push(...items);
|
||||
continue;
|
||||
}
|
||||
|
@ -133,6 +141,7 @@ export class MenuBuilder {
|
|||
parent: GroupModel | undefined,
|
||||
tag: TagInfo,
|
||||
depth: number,
|
||||
options: RedocNormalizedOptions,
|
||||
): OperationModel[] {
|
||||
if (tag.operations.length === 0) {
|
||||
return [];
|
||||
|
@ -140,7 +149,7 @@ export class MenuBuilder {
|
|||
|
||||
let res: OperationModel[] = [];
|
||||
for (let operationInfo of tag.operations) {
|
||||
let operation = new OperationModel(parser, operationInfo, parent);
|
||||
let operation = new OperationModel(parser, operationInfo, parent, options);
|
||||
operation.depth = depth;
|
||||
res.push(operation);
|
||||
}
|
||||
|
|
|
@ -1,20 +1,43 @@
|
|||
import { ThemeInterface } from '../theme';
|
||||
import { isNumeric } from '../utils/helpers';
|
||||
import defaultTheme from '../theme';
|
||||
|
||||
export interface RedocRawOptions {
|
||||
theme?: ThemeInterface;
|
||||
scrollYOffset?: number | string | Function;
|
||||
hideHostname?: boolean | string;
|
||||
expandResponses?: string | 'all';
|
||||
}
|
||||
|
||||
export class RedocNormalizedOptions {
|
||||
theme: ThemeInterface;
|
||||
scrollYOffset: () => number;
|
||||
hideHostname: boolean;
|
||||
expandResponses: { [code: string]: boolean } | 'all';
|
||||
|
||||
constructor(raw: RedocRawOptions) {
|
||||
this.theme = { ...(raw.theme || {}), ...defaultTheme }; // todo: merge deep
|
||||
this.scrollYOffset = RedocNormalizedOptions.normalizeScrollYOffset(raw.scrollYOffset);
|
||||
this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname);
|
||||
this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses);
|
||||
}
|
||||
|
||||
static normalizeExpandResponses(value: RedocRawOptions['expandResponses']) {
|
||||
if (value === 'all') {
|
||||
return 'all';
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const res = {};
|
||||
value.split(',').forEach(code => {
|
||||
res[code.trim()] = true;
|
||||
});
|
||||
return res;
|
||||
} else {
|
||||
console.warn(
|
||||
`expandResponses must be a string but received value "${value}" of type ${typeof value}`,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
static normalizeHideHostname(value: RedocRawOptions['hideHostname']): boolean {
|
||||
|
|
|
@ -6,6 +6,7 @@ import { observable, computed } from 'mobx';
|
|||
import { MenuBuilder } from './MenuBuilder';
|
||||
import { OpenAPIParser } from './OpenAPIParser';
|
||||
import { ApiInfoModel } from './models/ApiInfo';
|
||||
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
||||
|
||||
/**
|
||||
* Store that containts all the specification related information in the form of tree
|
||||
|
@ -13,7 +14,11 @@ import { ApiInfoModel } from './models/ApiInfo';
|
|||
export class SpecStore {
|
||||
@observable.ref parser: OpenAPIParser;
|
||||
|
||||
constructor(spec: OpenAPISpec, specUrl?: string) {
|
||||
constructor(
|
||||
spec: OpenAPISpec,
|
||||
specUrl: string | undefined,
|
||||
private options: RedocNormalizedOptions,
|
||||
) {
|
||||
this.parser = new OpenAPIParser(spec, specUrl);
|
||||
}
|
||||
|
||||
|
@ -29,7 +34,7 @@ export class SpecStore {
|
|||
|
||||
@computed
|
||||
get operationGroups() {
|
||||
return MenuBuilder.buildStructure(this.parser);
|
||||
return MenuBuilder.buildStructure(this.parser, this.options);
|
||||
}
|
||||
|
||||
@computed
|
||||
|
|
|
@ -14,6 +14,7 @@ import { CodeSample } from './types';
|
|||
import { OpenAPIParser } from '../OpenAPIParser';
|
||||
import { ContentItemModel, ExtendedOpenAPIOperation } from '../MenuBuilder';
|
||||
import { JsonPointer, getOperationSummary, isAbsolutePath, stripTrailingSlash } from '../../utils';
|
||||
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
||||
|
||||
function isNumeric(n) {
|
||||
return !isNaN(parseFloat(n)) && isFinite(n);
|
||||
|
@ -51,7 +52,12 @@ export class OperationModel implements IMenuItem {
|
|||
servers: OpenAPIServer[];
|
||||
codeSamples: CodeSample[];
|
||||
|
||||
constructor(parser: OpenAPIParser, operationSpec: ExtendedOpenAPIOperation, parent?: GroupModel) {
|
||||
constructor(
|
||||
parser: OpenAPIParser,
|
||||
operationSpec: ExtendedOpenAPIOperation,
|
||||
parent: GroupModel | undefined,
|
||||
options: RedocNormalizedOptions,
|
||||
) {
|
||||
this.id = operationSpec._$ref;
|
||||
this.name = getOperationSummary(operationSpec);
|
||||
this.description = operationSpec.description;
|
||||
|
@ -81,9 +87,15 @@ export class OperationModel implements IMenuItem {
|
|||
}
|
||||
return isNumeric(code) || code === 'default';
|
||||
}) // filter out other props (e.g. x-props)
|
||||
.map(
|
||||
code => new ResponseModel(parser, code, hasSuccessResponses, operationSpec.responses[code]),
|
||||
);
|
||||
.map(code => {
|
||||
return new ResponseModel(
|
||||
parser,
|
||||
code,
|
||||
hasSuccessResponses,
|
||||
operationSpec.responses[code],
|
||||
options,
|
||||
);
|
||||
});
|
||||
|
||||
this.servers = normalizeServers(
|
||||
parser.specUrl,
|
||||
|
|
|
@ -6,9 +6,10 @@ import { FieldModel } from './Field';
|
|||
import { MediaContentModel } from './MediaContent';
|
||||
import { OpenAPIParser } from '../OpenAPIParser';
|
||||
import { getStatusCodeType } from '../../utils';
|
||||
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
||||
|
||||
export class ResponseModel {
|
||||
@observable public expanded: boolean = false;
|
||||
@observable public expanded: boolean;
|
||||
|
||||
public content?: MediaContentModel;
|
||||
public code: string;
|
||||
|
@ -16,7 +17,15 @@ export class ResponseModel {
|
|||
public type: string;
|
||||
public headers: FieldModel[] = [];
|
||||
|
||||
constructor(parser: OpenAPIParser, code: string, defaultAsError: boolean, infoOrRef: Referenced<OpenAPIResponse>) {
|
||||
constructor(
|
||||
parser: OpenAPIParser,
|
||||
code: string,
|
||||
defaultAsError: boolean,
|
||||
infoOrRef: Referenced<OpenAPIResponse>,
|
||||
options: RedocNormalizedOptions,
|
||||
) {
|
||||
this.expanded = options.expandResponses === 'all' || options.expandResponses[code];
|
||||
|
||||
const info = parser.deref(infoOrRef);
|
||||
parser.exitRef(infoOrRef);
|
||||
this.code = code;
|
||||
|
|
Loading…
Reference in New Issue
Block a user