mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-29 12:03:44 +03:00
feat: use new Context API for options
This commit is contained in:
parent
ebb9b7ee52
commit
e0223494c2
14
package.json
14
package.json
|
@ -67,8 +67,8 @@
|
||||||
"prettier-eslint": "^8.8.1",
|
"prettier-eslint": "^8.8.1",
|
||||||
"puppeteer": "^1.2.0",
|
"puppeteer": "^1.2.0",
|
||||||
"raf": "^3.4.0",
|
"raf": "^3.4.0",
|
||||||
"react": "^16.2.0",
|
"react": "^16.3.0-alpha.2",
|
||||||
"react-dom": "^16.2.0",
|
"react-dom": "^16.3.0-alpha.2",
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
"shelljs": "^0.8.1",
|
"shelljs": "^0.8.1",
|
||||||
"source-map-loader": "^0.2.1",
|
"source-map-loader": "^0.2.1",
|
||||||
|
@ -158,8 +158,14 @@
|
||||||
"collectCoverageFrom": [
|
"collectCoverageFrom": [
|
||||||
"src/**/*.{ts,tsx}"
|
"src/**/*.{ts,tsx}"
|
||||||
],
|
],
|
||||||
"coverageReporters": ["json", "lcov", "text-summary"],
|
"coverageReporters": [
|
||||||
"coveragePathIgnorePatterns": ["\\.d\\.ts$"]
|
"json",
|
||||||
|
"lcov",
|
||||||
|
"text-summary"
|
||||||
|
],
|
||||||
|
"coveragePathIgnorePatterns": [
|
||||||
|
"\\.d\\.ts$"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ShelfIcon } from '../../common-elements';
|
import { ShelfIcon } from '../../common-elements';
|
||||||
import { OperationModel } from '../../services';
|
import { OperationModel } from '../../services';
|
||||||
import { ComponentWithOptions } from '../OptionsProvider';
|
import { OptionsContext } from '../OptionsProvider';
|
||||||
import { SelectOnClick } from '../SelectOnClick/SelectOnClick';
|
import { SelectOnClick } from '../SelectOnClick/SelectOnClick';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -25,7 +25,7 @@ export interface EndpointState {
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Endpoint extends ComponentWithOptions<EndpointProps, EndpointState> {
|
export class Endpoint extends React.Component<EndpointProps, EndpointState> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -38,13 +38,13 @@ export class Endpoint extends ComponentWithOptions<EndpointProps, EndpointState>
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { operation, inverted } = this.props;
|
const { operation, inverted, hideHostname } = this.props;
|
||||||
const { expanded } = this.state;
|
const { expanded } = this.state;
|
||||||
|
|
||||||
const hideHostname = this.props.hideHostname || this.options.hideHostname;
|
|
||||||
|
|
||||||
// TODO: highlight server variables, e.g. https://{user}.test.com
|
// TODO: highlight server variables, e.g. https://{user}.test.com
|
||||||
return (
|
return (
|
||||||
|
<OptionsContext.Consumer>
|
||||||
|
{options => (
|
||||||
<OperationEndpointWrap>
|
<OperationEndpointWrap>
|
||||||
<EndpointInfo onClick={this.toggle} expanded={expanded} inverted={inverted}>
|
<EndpointInfo onClick={this.toggle} expanded={expanded} inverted={inverted}>
|
||||||
<HttpVerb type={operation.httpVerb}> {operation.httpVerb}</HttpVerb>{' '}
|
<HttpVerb type={operation.httpVerb}> {operation.httpVerb}</HttpVerb>{' '}
|
||||||
|
@ -63,7 +63,7 @@ export class Endpoint extends ComponentWithOptions<EndpointProps, EndpointState>
|
||||||
<div>{server.description}</div>
|
<div>{server.description}</div>
|
||||||
<SelectOnClick>
|
<SelectOnClick>
|
||||||
<ServerUrl>
|
<ServerUrl>
|
||||||
{!hideHostname && <span>{server.url}</span>}
|
{!(hideHostname || options.hideHostname) && <span>{server.url}</span>}
|
||||||
{operation.path}
|
{operation.path}
|
||||||
</ServerUrl>
|
</ServerUrl>
|
||||||
</SelectOnClick>
|
</SelectOnClick>
|
||||||
|
@ -71,6 +71,8 @@ export class Endpoint extends ComponentWithOptions<EndpointProps, EndpointState>
|
||||||
))}
|
))}
|
||||||
</ServersOverlay>
|
</ServersOverlay>
|
||||||
</OperationEndpointWrap>
|
</OperationEndpointWrap>
|
||||||
|
)}
|
||||||
|
</OptionsContext.Consumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import styled from '../../styled-components';
|
||||||
|
|
||||||
import * as DOMPurify from 'dompurify';
|
import * as DOMPurify from 'dompurify';
|
||||||
import { AppStore, MarkdownRenderer } from '../../services';
|
import { AppStore, MarkdownRenderer } from '../../services';
|
||||||
import { ComponentWithOptions } from '../OptionsProvider';
|
import { OptionsContext } from '../OptionsProvider';
|
||||||
|
|
||||||
import { markdownCss } from './styles';
|
import { markdownCss } from './styles';
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ export interface MarkdownProps {
|
||||||
store?: AppStore;
|
store?: AppStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
class InternalMarkdown extends ComponentWithOptions<MarkdownProps> {
|
class InternalMarkdown extends React.Component<MarkdownProps> {
|
||||||
constructor(props: MarkdownProps) {
|
constructor(props: MarkdownProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -40,10 +40,7 @@ class InternalMarkdown extends ComponentWithOptions<MarkdownProps> {
|
||||||
throw new Error('When using components with Markdwon in ReDoc, store prop must be provided');
|
throw new Error('When using components with Markdwon in ReDoc, store prop must be provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitize =
|
const sanitize = (untrustedSpec, html) => (untrustedSpec ? DOMPurify.sanitize(html) : html);
|
||||||
this.props.sanitize || this.options.untrustedSpec
|
|
||||||
? (html: string) => DOMPurify.sanitize(html)
|
|
||||||
: (html: string) => html;
|
|
||||||
|
|
||||||
const renderer = new MarkdownRenderer();
|
const renderer = new MarkdownRenderer();
|
||||||
const parts = components
|
const parts = components
|
||||||
|
@ -62,26 +59,36 @@ class InternalMarkdown extends ComponentWithOptions<MarkdownProps> {
|
||||||
appendClass += ' -inline';
|
appendClass += ' -inline';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inline) {
|
|
||||||
return (
|
return (
|
||||||
|
<OptionsContext.Consumer>
|
||||||
|
{options =>
|
||||||
|
inline ? (
|
||||||
<span
|
<span
|
||||||
className={className + appendClass}
|
className={className + appendClass}
|
||||||
dangerouslySetInnerHTML={{ __html: sanitize(parts[0] as string) }}
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: sanitize(options.untrustedSpec, parts[0] as string),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
) : (
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className + appendClass}>
|
<div className={className + appendClass}>
|
||||||
{parts.map(
|
{parts.map(
|
||||||
(part, idx) =>
|
(part, idx) =>
|
||||||
typeof part === 'string' ? (
|
typeof part === 'string' ? (
|
||||||
<div key={idx} dangerouslySetInnerHTML={{ __html: sanitize(part) }} />
|
<div
|
||||||
|
key={idx}
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitize(options.untrustedSpec, part) }}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<part.component key={idx} {...{ ...part.attrs, ...part.propsSelector(store) }} />
|
<part.component
|
||||||
|
key={idx}
|
||||||
|
{...{ ...part.attrs, ...part.propsSelector(store) }}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</OptionsContext.Consumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { observer } from 'mobx-react';
|
||||||
|
|
||||||
import { Badge, DarkRightPanel, H2, MiddlePanel, Row } from '../../common-elements';
|
import { Badge, DarkRightPanel, H2, MiddlePanel, Row } from '../../common-elements';
|
||||||
|
|
||||||
import { ComponentWithOptions } from '../OptionsProvider';
|
import { OptionsContext } from '../OptionsProvider';
|
||||||
|
|
||||||
import { ShareLink } from '../../common-elements/linkify';
|
import { ShareLink } from '../../common-elements/linkify';
|
||||||
import { Endpoint } from '../Endpoint/Endpoint';
|
import { Endpoint } from '../Endpoint/Endpoint';
|
||||||
|
@ -40,31 +40,34 @@ interface OperationProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class Operation extends ComponentWithOptions<OperationProps> {
|
export class Operation extends React.Component<OperationProps> {
|
||||||
render() {
|
render() {
|
||||||
const { operation } = this.props;
|
const { operation } = this.props;
|
||||||
|
|
||||||
const { name: summary, description, deprecated } = operation;
|
const { name: summary, description, deprecated } = operation;
|
||||||
const pathInMiddle = this.options.pathInMiddlePanel;
|
|
||||||
return (
|
return (
|
||||||
|
<OptionsContext.Consumer>
|
||||||
|
{options => (
|
||||||
<OperationRow>
|
<OperationRow>
|
||||||
<MiddlePanel>
|
<MiddlePanel>
|
||||||
<H2>
|
<H2>
|
||||||
<ShareLink href={'#' + operation.getHash()} />
|
<ShareLink href={'#' + operation.getHash()} />
|
||||||
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
|
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
|
||||||
</H2>
|
</H2>
|
||||||
{pathInMiddle && <Endpoint operation={operation} inverted={true} />}
|
{options.pathInMiddlePanel && <Endpoint operation={operation} inverted={true} />}
|
||||||
{description !== undefined && <Markdown source={description} />}
|
{description !== undefined && <Markdown source={description} />}
|
||||||
<SecurityRequirements securities={operation.security} />
|
<SecurityRequirements securities={operation.security} />
|
||||||
<Parameters parameters={operation.parameters} body={operation.requestBody} />
|
<Parameters parameters={operation.parameters} body={operation.requestBody} />
|
||||||
<ResponsesList responses={operation.responses} />
|
<ResponsesList responses={operation.responses} />
|
||||||
</MiddlePanel>
|
</MiddlePanel>
|
||||||
<DarkRightPanel>
|
<DarkRightPanel>
|
||||||
{!pathInMiddle && <Endpoint operation={operation} />}
|
{!options.pathInMiddlePanel && <Endpoint operation={operation} />}
|
||||||
<RequestSamples operation={operation} />
|
<RequestSamples operation={operation} />
|
||||||
<ResponseSamples operation={operation} />
|
<ResponseSamples operation={operation} />
|
||||||
</DarkRightPanel>
|
</DarkRightPanel>
|
||||||
</OperationRow>
|
</OperationRow>
|
||||||
|
)}
|
||||||
|
</OptionsContext.Consumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,32 +3,19 @@ import * as React from 'react';
|
||||||
|
|
||||||
import { RedocNormalizedOptions } from '../services/RedocNormalizedOptions';
|
import { RedocNormalizedOptions } from '../services/RedocNormalizedOptions';
|
||||||
|
|
||||||
export interface OptionsProviderProps {
|
// TODO: contribute declarations to @types/react once 16.3 is released
|
||||||
options: RedocNormalizedOptions;
|
type ReactProviderComponent<T> = React.ComponentType<{ value: T }>;
|
||||||
|
type ReactConsumerComponent<T> = React.ComponentType<{ children: ((value: T) => React.ReactNode) }>;
|
||||||
|
|
||||||
|
interface ReactContext<T> {
|
||||||
|
Provider: ReactProviderComponent<T>;
|
||||||
|
Consumer: ReactConsumerComponent<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OptionsProvider extends React.Component<OptionsProviderProps> {
|
declare module 'react' {
|
||||||
static childContextTypes = {
|
function createContext<T>(defatulValue: T): ReactContext<T>;
|
||||||
redocOptions: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
getChildContext() {
|
|
||||||
return {
|
|
||||||
redocOptions: this.props.options,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
export const OptionsContext = React.createContext(new RedocNormalizedOptions({}));
|
||||||
return React.Children.only(this.props.children);
|
export const OptionsProvider = OptionsContext.Provider;
|
||||||
}
|
export const OptionsConsumer = OptionsContext.Consumer;
|
||||||
}
|
|
||||||
|
|
||||||
export class ComponentWithOptions<P = {}, S = {}> extends React.Component<P, S> {
|
|
||||||
static contextTypes = {
|
|
||||||
redocOptions: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
get options(): RedocNormalizedOptions {
|
|
||||||
return this.context.redocOptions || {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ export class Redoc extends React.Component<RedocProps> {
|
||||||
const store = this.props.store;
|
const store = this.props.store;
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={options.theme}>
|
<ThemeProvider theme={options.theme}>
|
||||||
<OptionsProvider options={options}>
|
<OptionsProvider value={options}>
|
||||||
<RedocWrap className="redoc-wrap">
|
<RedocWrap className="redoc-wrap">
|
||||||
<StickyResponsiveSidebar menu={menu} className="menu-content">
|
<StickyResponsiveSidebar menu={menu} className="menu-content">
|
||||||
<ApiLogo info={spec.info} />
|
<ApiLogo info={spec.info} />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ComponentWithOptions } from '../OptionsProvider';
|
import { OptionsContext } from '../OptionsProvider';
|
||||||
|
|
||||||
import { IMenuItem, MenuStore } from '../../services/MenuStore';
|
import { IMenuItem, MenuStore } from '../../services/MenuStore';
|
||||||
import { MenuItems } from './MenuItems';
|
import { MenuItems } from './MenuItems';
|
||||||
|
@ -8,13 +8,15 @@ import { MenuItems } from './MenuItems';
|
||||||
import { PerfectScrollbar } from '../../common-elements/perfect-scrollbar';
|
import { PerfectScrollbar } from '../../common-elements/perfect-scrollbar';
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class SideMenu extends ComponentWithOptions<{ menu: MenuStore }> {
|
export class SideMenu extends React.Component<{ menu: MenuStore }> {
|
||||||
private _updateScroll?: () => void;
|
private _updateScroll?: () => void;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const store = this.props.menu;
|
const store = this.props.menu;
|
||||||
const nativeScrollbars = this.options.nativeScrollbars;
|
return (
|
||||||
return nativeScrollbars ? (
|
<OptionsContext.Consumer>
|
||||||
|
{options =>
|
||||||
|
options.nativeScrollbars ? (
|
||||||
<MenuItems
|
<MenuItems
|
||||||
style={{
|
style={{
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
|
@ -27,6 +29,9 @@ export class SideMenu extends ComponentWithOptions<{ menu: MenuStore }> {
|
||||||
<PerfectScrollbar updateFn={this.saveScrollUpdate}>
|
<PerfectScrollbar updateFn={this.saveScrollUpdate}>
|
||||||
<MenuItems items={store.items} onActivate={this.activate} />
|
<MenuItems items={store.items} onActivate={this.activate} />
|
||||||
</PerfectScrollbar>
|
</PerfectScrollbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</OptionsContext.Consumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as React from 'react';
|
||||||
import { MenuStore } from '../../services/MenuStore';
|
import { MenuStore } from '../../services/MenuStore';
|
||||||
import { RedocNormalizedOptions, RedocRawOptions } from '../../services/RedocNormalizedOptions';
|
import { RedocNormalizedOptions, RedocRawOptions } from '../../services/RedocNormalizedOptions';
|
||||||
import styled, { media, withProps } from '../../styled-components';
|
import styled, { media, withProps } from '../../styled-components';
|
||||||
import { ComponentWithOptions } from '../OptionsProvider';
|
import { OptionsContext } from '../OptionsProvider';
|
||||||
import { AnimatedChevronButton } from './ChevronSvg';
|
import { AnimatedChevronButton } from './ChevronSvg';
|
||||||
|
|
||||||
let Stickyfill;
|
let Stickyfill;
|
||||||
|
@ -67,7 +67,7 @@ const FloatingButton = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class StickyResponsiveSidebar extends ComponentWithOptions<StickySidebarProps> {
|
export class StickyResponsiveSidebar extends React.Component<StickySidebarProps> {
|
||||||
stickyElement: Element;
|
stickyElement: Element;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -82,28 +82,29 @@ export class StickyResponsiveSidebar extends ComponentWithOptions<StickySidebarP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get scrollYOffset() {
|
getScrollYOffset(options) {
|
||||||
let top;
|
let top;
|
||||||
if (this.props.scrollYOffset !== undefined) {
|
if (this.props.scrollYOffset !== undefined) {
|
||||||
top = RedocNormalizedOptions.normalizeScrollYOffset(this.props.scrollYOffset)();
|
top = RedocNormalizedOptions.normalizeScrollYOffset(this.props.scrollYOffset)();
|
||||||
} else {
|
} else {
|
||||||
top = this.options.scrollYOffset();
|
top = options.scrollYOffset();
|
||||||
}
|
}
|
||||||
return top + 'px';
|
return top + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const top = this.scrollYOffset;
|
|
||||||
const open = this.props.menu.sideBarOpened;
|
const open = this.props.menu.sideBarOpened;
|
||||||
|
|
||||||
const height = `calc(100vh - ${top})`;
|
const height = `calc(100vh - ${top})`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<OptionsContext.Consumer>
|
||||||
|
{options => (
|
||||||
<>
|
<>
|
||||||
<StyledStickySidebar
|
<StyledStickySidebar
|
||||||
open={open}
|
open={open}
|
||||||
className={this.props.className}
|
className={this.props.className}
|
||||||
style={{ top, height }}
|
style={{ top: this.getScrollYOffset(options), height }}
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
innerRef={el => {
|
innerRef={el => {
|
||||||
this.stickyElement = el;
|
this.stickyElement = el;
|
||||||
|
@ -115,6 +116,8 @@ export class StickyResponsiveSidebar extends ComponentWithOptions<StickySidebarP
|
||||||
<AnimatedChevronButton open={open} />
|
<AnimatedChevronButton open={open} />
|
||||||
</FloatingButton>
|
</FloatingButton>
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
</OptionsContext.Consumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -7547,9 +7547,9 @@ rc@^1.1.7:
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
react-dom@^16.2.0:
|
react-dom@^16.3.0-alpha.2:
|
||||||
version "16.2.0"
|
version "16.3.0-alpha.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.2.0.tgz#69003178601c0ca19b709b33a83369fe6124c044"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.3.0-alpha.2.tgz#a970b6185684941e89a568c09321d22643457cb6"
|
||||||
dependencies:
|
dependencies:
|
||||||
fbjs "^0.8.16"
|
fbjs "^0.8.16"
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
|
@ -7596,9 +7596,9 @@ react-test-renderer@^16.0.0-0:
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
prop-types "^15.6.0"
|
prop-types "^15.6.0"
|
||||||
|
|
||||||
react@^16.2.0:
|
react@^16.3.0-alpha.2:
|
||||||
version "16.2.0"
|
version "16.3.0-alpha.2"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"
|
resolved "https://registry.yarnpkg.com/react/-/react-16.3.0-alpha.2.tgz#91e2b82bb985b23e7b6555f810e1fd94894afce2"
|
||||||
dependencies:
|
dependencies:
|
||||||
fbjs "^0.8.16"
|
fbjs "^0.8.16"
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user