Skip to content

Commit

Permalink
feat: use new Context API for options
Browse files Browse the repository at this point in the history
  • Loading branch information
RomanHotsiy committed Mar 17, 2018
1 parent ebb9b7e commit e022349
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 150 deletions.
14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@
"prettier-eslint": "^8.8.1",
"puppeteer": "^1.2.0",
"raf": "^3.4.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react": "^16.3.0-alpha.2",
"react-dom": "^16.3.0-alpha.2",
"rimraf": "^2.6.2",
"shelljs": "^0.8.1",
"source-map-loader": "^0.2.1",
Expand Down Expand Up @@ -158,8 +158,14 @@
"collectCoverageFrom": [
"src/**/*.{ts,tsx}"
],
"coverageReporters": ["json", "lcov", "text-summary"],
"coveragePathIgnorePatterns": ["\\.d\\.ts$"]
"coverageReporters": [
"json",
"lcov",
"text-summary"
],
"coveragePathIgnorePatterns": [
"\\.d\\.ts$"
]
},
"prettier": {
"singleQuote": true,
Expand Down
64 changes: 33 additions & 31 deletions src/components/Endpoint/Endpoint.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import { ShelfIcon } from '../../common-elements';
import { OperationModel } from '../../services';
import { ComponentWithOptions } from '../OptionsProvider';
import { OptionsContext } from '../OptionsProvider';
import { SelectOnClick } from '../SelectOnClick/SelectOnClick';

import {
Expand All @@ -25,7 +25,7 @@ export interface EndpointState {
expanded: boolean;
}

export class Endpoint extends ComponentWithOptions<EndpointProps, EndpointState> {
export class Endpoint extends React.Component<EndpointProps, EndpointState> {
constructor(props) {
super(props);
this.state = {
Expand All @@ -38,39 +38,41 @@ export class Endpoint extends ComponentWithOptions<EndpointProps, EndpointState>
};

render() {
const { operation, inverted } = this.props;
const { operation, inverted, hideHostname } = this.props;
const { expanded } = this.state;

const hideHostname = this.props.hideHostname || this.options.hideHostname;

// TODO: highlight server variables, e.g. https://{user}.test.com
return (
<OperationEndpointWrap>
<EndpointInfo onClick={this.toggle} expanded={expanded} inverted={inverted}>
<HttpVerb type={operation.httpVerb}> {operation.httpVerb}</HttpVerb>{' '}
<ServerRelativeURL>{operation.path}</ServerRelativeURL>
<ShelfIcon
float={'right'}
color={inverted ? 'black' : 'white'}
size={'20px'}
direction={expanded ? 'up' : 'down'}
style={{ marginRight: '-25px' }}
/>
</EndpointInfo>
<ServersOverlay expanded={expanded}>
{operation.servers.map(server => (
<ServerItem key={server.url}>
<div>{server.description}</div>
<SelectOnClick>
<ServerUrl>
{!hideHostname && <span>{server.url}</span>}
{operation.path}
</ServerUrl>
</SelectOnClick>
</ServerItem>
))}
</ServersOverlay>
</OperationEndpointWrap>
<OptionsContext.Consumer>
{options => (
<OperationEndpointWrap>
<EndpointInfo onClick={this.toggle} expanded={expanded} inverted={inverted}>
<HttpVerb type={operation.httpVerb}> {operation.httpVerb}</HttpVerb>{' '}
<ServerRelativeURL>{operation.path}</ServerRelativeURL>
<ShelfIcon
float={'right'}
color={inverted ? 'black' : 'white'}
size={'20px'}
direction={expanded ? 'up' : 'down'}
style={{ marginRight: '-25px' }}
/>
</EndpointInfo>
<ServersOverlay expanded={expanded}>
{operation.servers.map(server => (
<ServerItem key={server.url}>
<div>{server.description}</div>
<SelectOnClick>
<ServerUrl>
{!(hideHostname || options.hideHostname) && <span>{server.url}</span>}
{operation.path}
</ServerUrl>
</SelectOnClick>
</ServerItem>
))}
</ServersOverlay>
</OperationEndpointWrap>
)}
</OptionsContext.Consumer>
);
}
}
57 changes: 32 additions & 25 deletions src/components/Markdown/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import styled from '../../styled-components';

import * as DOMPurify from 'dompurify';
import { AppStore, MarkdownRenderer } from '../../services';
import { ComponentWithOptions } from '../OptionsProvider';
import { OptionsContext } from '../OptionsProvider';

import { markdownCss } from './styles';

Expand All @@ -24,7 +24,7 @@ export interface MarkdownProps {
store?: AppStore;
}

class InternalMarkdown extends ComponentWithOptions<MarkdownProps> {
class InternalMarkdown extends React.Component<MarkdownProps> {
constructor(props: MarkdownProps) {
super(props);

Expand All @@ -40,10 +40,7 @@ class InternalMarkdown extends ComponentWithOptions<MarkdownProps> {
throw new Error('When using components with Markdwon in ReDoc, store prop must be provided');
}

const sanitize =
this.props.sanitize || this.options.untrustedSpec
? (html: string) => DOMPurify.sanitize(html)
: (html: string) => html;
const sanitize = (untrustedSpec, html) => (untrustedSpec ? DOMPurify.sanitize(html) : html);

const renderer = new MarkdownRenderer();
const parts = components
Expand All @@ -62,26 +59,36 @@ class InternalMarkdown extends ComponentWithOptions<MarkdownProps> {
appendClass += ' -inline';
}

if (inline) {
return (
<span
className={className + appendClass}
dangerouslySetInnerHTML={{ __html: sanitize(parts[0] as string) }}
/>
);
}

return (
<div className={className + appendClass}>
{parts.map(
(part, idx) =>
typeof part === 'string' ? (
<div key={idx} dangerouslySetInnerHTML={{ __html: sanitize(part) }} />
) : (
<part.component key={idx} {...{ ...part.attrs, ...part.propsSelector(store) }} />
),
)}
</div>
<OptionsContext.Consumer>
{options =>
inline ? (
<span
className={className + appendClass}
dangerouslySetInnerHTML={{
__html: sanitize(options.untrustedSpec, parts[0] as string),
}}
/>
) : (
<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>
);
}
}
Expand Down
45 changes: 24 additions & 21 deletions src/components/Operation/Operation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { observer } from 'mobx-react';

import { Badge, DarkRightPanel, H2, MiddlePanel, Row } from '../../common-elements';

import { ComponentWithOptions } from '../OptionsProvider';
import { OptionsContext } from '../OptionsProvider';

import { ShareLink } from '../../common-elements/linkify';
import { Endpoint } from '../Endpoint/Endpoint';
Expand Down Expand Up @@ -40,31 +40,34 @@ interface OperationProps {
}

@observer
export class Operation extends ComponentWithOptions<OperationProps> {
export class Operation extends React.Component<OperationProps> {
render() {
const { operation } = this.props;

const { name: summary, description, deprecated } = operation;
const pathInMiddle = this.options.pathInMiddlePanel;
return (
<OperationRow>
<MiddlePanel>
<H2>
<ShareLink href={'#' + operation.getHash()} />
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
</H2>
{pathInMiddle && <Endpoint operation={operation} inverted={true} />}
{description !== undefined && <Markdown source={description} />}
<SecurityRequirements securities={operation.security} />
<Parameters parameters={operation.parameters} body={operation.requestBody} />
<ResponsesList responses={operation.responses} />
</MiddlePanel>
<DarkRightPanel>
{!pathInMiddle && <Endpoint operation={operation} />}
<RequestSamples operation={operation} />
<ResponseSamples operation={operation} />
</DarkRightPanel>
</OperationRow>
<OptionsContext.Consumer>
{options => (
<OperationRow>
<MiddlePanel>
<H2>
<ShareLink href={'#' + operation.getHash()} />
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
</H2>
{options.pathInMiddlePanel && <Endpoint operation={operation} inverted={true} />}
{description !== undefined && <Markdown source={description} />}
<SecurityRequirements securities={operation.security} />
<Parameters parameters={operation.parameters} body={operation.requestBody} />
<ResponsesList responses={operation.responses} />
</MiddlePanel>
<DarkRightPanel>
{!options.pathInMiddlePanel && <Endpoint operation={operation} />}
<RequestSamples operation={operation} />
<ResponseSamples operation={operation} />
</DarkRightPanel>
</OperationRow>
)}
</OptionsContext.Consumer>
);
}
}
37 changes: 12 additions & 25 deletions src/components/OptionsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,19 @@ import * as React from 'react';

import { RedocNormalizedOptions } from '../services/RedocNormalizedOptions';

export interface OptionsProviderProps {
options: RedocNormalizedOptions;
}

export class OptionsProvider extends React.Component<OptionsProviderProps> {
static childContextTypes = {
redocOptions: PropTypes.object.isRequired,
};

getChildContext() {
return {
redocOptions: this.props.options,
};
}
// TODO: contribute declarations to @types/react once 16.3 is released
type ReactProviderComponent<T> = React.ComponentType<{ value: T }>;
type ReactConsumerComponent<T> = React.ComponentType<{ children: ((value: T) => React.ReactNode) }>;

render() {
return React.Children.only(this.props.children);
}
interface ReactContext<T> {
Provider: ReactProviderComponent<T>;
Consumer: ReactConsumerComponent<T>;
}

export class ComponentWithOptions<P = {}, S = {}> extends React.Component<P, S> {
static contextTypes = {
redocOptions: PropTypes.object,
};

get options(): RedocNormalizedOptions {
return this.context.redocOptions || {};
}
declare module 'react' {
function createContext<T>(defatulValue: T): ReactContext<T>;
}

export const OptionsContext = React.createContext(new RedocNormalizedOptions({}));
export const OptionsProvider = OptionsContext.Provider;
export const OptionsConsumer = OptionsContext.Consumer;
2 changes: 1 addition & 1 deletion src/components/Redoc/Redoc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class Redoc extends React.Component<RedocProps> {
const store = this.props.store;
return (
<ThemeProvider theme={options.theme}>
<OptionsProvider options={options}>
<OptionsProvider value={options}>
<RedocWrap className="redoc-wrap">
<StickyResponsiveSidebar menu={menu} className="menu-content">
<ApiLogo info={spec.info} />
Expand Down
37 changes: 21 additions & 16 deletions src/components/SideMenu/SideMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import { ComponentWithOptions } from '../OptionsProvider';
import { OptionsContext } from '../OptionsProvider';

import { IMenuItem, MenuStore } from '../../services/MenuStore';
import { MenuItems } from './MenuItems';

import { PerfectScrollbar } from '../../common-elements/perfect-scrollbar';

@observer
export class SideMenu extends ComponentWithOptions<{ menu: MenuStore }> {
export class SideMenu extends React.Component<{ menu: MenuStore }> {
private _updateScroll?: () => void;

render() {
const store = this.props.menu;
const nativeScrollbars = this.options.nativeScrollbars;
return nativeScrollbars ? (
<MenuItems
style={{
overflow: 'auto',
msOverflowStyle: '-ms-autohiding-scrollbar',
}}
items={store.items}
onActivate={this.activate}
/>
) : (
<PerfectScrollbar updateFn={this.saveScrollUpdate}>
<MenuItems items={store.items} onActivate={this.activate} />
</PerfectScrollbar>
return (
<OptionsContext.Consumer>
{options =>
options.nativeScrollbars ? (
<MenuItems
style={{
overflow: 'auto',
msOverflowStyle: '-ms-autohiding-scrollbar',
}}
items={store.items}
onActivate={this.activate}
/>
) : (
<PerfectScrollbar updateFn={this.saveScrollUpdate}>
<MenuItems items={store.items} onActivate={this.activate} />
</PerfectScrollbar>
)
}
</OptionsContext.Consumer>
);
}

Expand Down
Loading

0 comments on commit e022349

Please sign in to comment.