feat: use new Context API for options

This commit is contained in:
Roman Hotsiy 2018-03-17 22:04:37 +02:00
parent ebb9b7ee52
commit e0223494c2
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
9 changed files with 163 additions and 150 deletions

View File

@ -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,

View File

@ -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,39 +38,41 @@ 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 (
<OperationEndpointWrap> <OptionsContext.Consumer>
<EndpointInfo onClick={this.toggle} expanded={expanded} inverted={inverted}> {options => (
<HttpVerb type={operation.httpVerb}> {operation.httpVerb}</HttpVerb>{' '} <OperationEndpointWrap>
<ServerRelativeURL>{operation.path}</ServerRelativeURL> <EndpointInfo onClick={this.toggle} expanded={expanded} inverted={inverted}>
<ShelfIcon <HttpVerb type={operation.httpVerb}> {operation.httpVerb}</HttpVerb>{' '}
float={'right'} <ServerRelativeURL>{operation.path}</ServerRelativeURL>
color={inverted ? 'black' : 'white'} <ShelfIcon
size={'20px'} float={'right'}
direction={expanded ? 'up' : 'down'} color={inverted ? 'black' : 'white'}
style={{ marginRight: '-25px' }} size={'20px'}
/> direction={expanded ? 'up' : 'down'}
</EndpointInfo> style={{ marginRight: '-25px' }}
<ServersOverlay expanded={expanded}> />
{operation.servers.map(server => ( </EndpointInfo>
<ServerItem key={server.url}> <ServersOverlay expanded={expanded}>
<div>{server.description}</div> {operation.servers.map(server => (
<SelectOnClick> <ServerItem key={server.url}>
<ServerUrl> <div>{server.description}</div>
{!hideHostname && <span>{server.url}</span>} <SelectOnClick>
{operation.path} <ServerUrl>
</ServerUrl> {!(hideHostname || options.hideHostname) && <span>{server.url}</span>}
</SelectOnClick> {operation.path}
</ServerItem> </ServerUrl>
))} </SelectOnClick>
</ServersOverlay> </ServerItem>
</OperationEndpointWrap> ))}
</ServersOverlay>
</OperationEndpointWrap>
)}
</OptionsContext.Consumer>
); );
} }
} }

View File

@ -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 (
<span
className={className + appendClass}
dangerouslySetInnerHTML={{ __html: sanitize(parts[0] as string) }}
/>
);
}
return ( return (
<div className={className + appendClass}> <OptionsContext.Consumer>
{parts.map( {options =>
(part, idx) => inline ? (
typeof part === 'string' ? ( <span
<div key={idx} dangerouslySetInnerHTML={{ __html: sanitize(part) }} /> className={className + appendClass}
) : ( dangerouslySetInnerHTML={{
<part.component key={idx} {...{ ...part.attrs, ...part.propsSelector(store) }} /> __html: sanitize(options.untrustedSpec, parts[0] as string),
), }}
)} />
</div> ) : (
<div className={className + appendClass}>
{parts.map(
(part, idx) =>
typeof part === 'string' ? (
<div
key={idx}
dangerouslySetInnerHTML={{ __html: sanitize(options.untrustedSpec, part) }}
/>
) : (
<part.component
key={idx}
{...{ ...part.attrs, ...part.propsSelector(store) }}
/>
),
)}
</div>
)
}
</OptionsContext.Consumer>
); );
} }
} }

View File

@ -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 (
<OperationRow> <OptionsContext.Consumer>
<MiddlePanel> {options => (
<H2> <OperationRow>
<ShareLink href={'#' + operation.getHash()} /> <MiddlePanel>
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>} <H2>
</H2> <ShareLink href={'#' + operation.getHash()} />
{pathInMiddle && <Endpoint operation={operation} inverted={true} />} {summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
{description !== undefined && <Markdown source={description} />} </H2>
<SecurityRequirements securities={operation.security} /> {options.pathInMiddlePanel && <Endpoint operation={operation} inverted={true} />}
<Parameters parameters={operation.parameters} body={operation.requestBody} /> {description !== undefined && <Markdown source={description} />}
<ResponsesList responses={operation.responses} /> <SecurityRequirements securities={operation.security} />
</MiddlePanel> <Parameters parameters={operation.parameters} body={operation.requestBody} />
<DarkRightPanel> <ResponsesList responses={operation.responses} />
{!pathInMiddle && <Endpoint operation={operation} />} </MiddlePanel>
<RequestSamples operation={operation} /> <DarkRightPanel>
<ResponseSamples operation={operation} /> {!options.pathInMiddlePanel && <Endpoint operation={operation} />}
</DarkRightPanel> <RequestSamples operation={operation} />
</OperationRow> <ResponseSamples operation={operation} />
</DarkRightPanel>
</OperationRow>
)}
</OptionsContext.Consumer>
); );
} }
} }

View File

@ -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() {
return React.Children.only(this.props.children);
}
} }
export class ComponentWithOptions<P = {}, S = {}> extends React.Component<P, S> { export const OptionsContext = React.createContext(new RedocNormalizedOptions({}));
static contextTypes = { export const OptionsProvider = OptionsContext.Provider;
redocOptions: PropTypes.object, export const OptionsConsumer = OptionsContext.Consumer;
};
get options(): RedocNormalizedOptions {
return this.context.redocOptions || {};
}
}

View File

@ -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} />

View File

@ -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,25 +8,30 @@ 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>
<MenuItems {options =>
style={{ options.nativeScrollbars ? (
overflow: 'auto', <MenuItems
msOverflowStyle: '-ms-autohiding-scrollbar', style={{
}} overflow: 'auto',
items={store.items} msOverflowStyle: '-ms-autohiding-scrollbar',
onActivate={this.activate} }}
/> items={store.items}
) : ( onActivate={this.activate}
<PerfectScrollbar updateFn={this.saveScrollUpdate}> />
<MenuItems items={store.items} onActivate={this.activate} /> ) : (
</PerfectScrollbar> <PerfectScrollbar updateFn={this.saveScrollUpdate}>
<MenuItems items={store.items} onActivate={this.activate} />
</PerfectScrollbar>
)
}
</OptionsContext.Consumer>
); );
} }

View File

@ -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,39 +82,42 @@ 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>
<StyledStickySidebar {options => (
open={open} <>
className={this.props.className} <StyledStickySidebar
style={{ top, height }} open={open}
// tslint:disable-next-line className={this.props.className}
innerRef={el => { style={{ top: this.getScrollYOffset(options), height }}
this.stickyElement = el; // tslint:disable-next-line
}} innerRef={el => {
> this.stickyElement = el;
{this.props.children} }}
</StyledStickySidebar> >
<FloatingButton onClick={this.toggleNavMenu}> {this.props.children}
<AnimatedChevronButton open={open} /> </StyledStickySidebar>
</FloatingButton> <FloatingButton onClick={this.toggleNavMenu}>
</> <AnimatedChevronButton open={open} />
</FloatingButton>
</>
)}
</OptionsContext.Consumer>
); );
} }

View File

@ -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"