Match rendering of callbacks to style of operation responses

This commit is contained in:
Jonathan Bailey 2020-01-16 15:26:20 -05:00
parent 01809ca644
commit 4bd2e70940
9 changed files with 156 additions and 44 deletions

View File

@ -0,0 +1,87 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import { isPayloadSample, RedocNormalizedOptions } from '../../services';
import { PayloadSamples } from '../PayloadSamples/PayloadSamples';
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
import { RightPanelHeader, Tab, TabList, TabPanel, Tabs } from '../../common-elements';
import { OptionsContext } from '../OptionsProvider';
import { CallbackModel } from '../../services/models';
import { Endpoint } from '../Endpoint/Endpoint';
export interface CallbackSamplesProps {
callbacks: CallbackModel[];
}
@observer
export class CallbackSamples extends React.Component<CallbackSamplesProps> {
static contextType = OptionsContext;
context: RedocNormalizedOptions;
render() {
const { callbacks } = this.props;
// Sums number of code samples per operation per callback
const numSamples = callbacks.reduce(
(callbackSum, callback) =>
callbackSum +
callback.operations.reduce(
(sampleSum, operation) => sampleSum + operation.codeSamples.length,
0,
),
0,
);
const hasSamples = numSamples > 0;
const hideTabList = numSamples === 1 ? this.context.hideSingleRequestSampleTab : false;
const renderTabs = () => {
return callbacks.map(callback => {
return callback.operations.map(operation => {
return operation.codeSamples.map(sample => {
return (
<Tab key={operation.id + '_' + operation.name}>
{operation.name} {sample.label !== undefined ? sample.label : sample.lang}
</Tab>
);
});
});
});
};
const renderTabPanels = () => {
return callbacks.map(callback => {
return callback.operations.map(operation => {
return operation.codeSamples.map(sample => {
return (
<TabPanel key={sample.lang + '_' + (sample.label || '')}>
{isPayloadSample(sample) ? (
<div>
<Endpoint operation={operation} inverted={false} />
<PayloadSamples content={sample.requestBodyContent} />
</div>
) : (
<SourceCodeWithCopy lang={sample.lang} source={sample.source} />
)}
</TabPanel>
);
});
});
});
};
return (
(hasSamples && (
<div>
<RightPanelHeader> Callback samples </RightPanelHeader>
<Tabs defaultIndex={0}>
<TabList hidden={hideTabList}>{renderTabs()}</TabList>
{renderTabPanels()}
</Tabs>
</div>
)) ||
null
);
}
}

View File

@ -1,23 +1,29 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import { CallbackModel } from '../../services/models'; import { OperationModel } from '../../services/models';
import { CallbackDetails } from './CallbackDetails';
import { CallbackDetailsWrap, StyledCallbackTitle } from '../Callbacks/styled.elements'; import { CallbackDetailsWrap, StyledCallbackTitle } from '../Callbacks/styled.elements';
@observer @observer
export class CallbackView extends React.Component<{ callback: CallbackModel }> { export class CallbackView extends React.Component<{ callbackOperation: OperationModel }> {
toggle = () => { toggle = () => {
this.props.callback.toggle(); this.props.callbackOperation.toggle();
}; };
render() { render() {
const { name, expanded } = this.props.callback; const { name, description, expanded } = this.props.callbackOperation;
return ( return (
<div> <div>
<StyledCallbackTitle onClick={this.toggle} name={name} opened={expanded} /> <StyledCallbackTitle
onClick={this.toggle}
name={name}
description={description}
opened={expanded}
/>
{expanded && ( {expanded && (
<CallbackDetailsWrap> <CallbackDetailsWrap>
<span>{name}</span> <CallbackDetails callbackOperation={this.props.callbackOperation} />
</CallbackDetailsWrap> </CallbackDetailsWrap>
)} )}
</div> </div>

View File

@ -0,0 +1,9 @@
import * as React from 'react';
import { OperationModel } from '../../services/models';
import { OperationItem } from '../ContentItems/ContentItems';
export class CallbackDetails extends React.PureComponent<{ callbackOperation: OperationModel }> {
render() {
return <OperationItem item={this.props.callbackOperation} />;
}
}

View File

@ -1,9 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import { ShelfIcon } from '../../common-elements'; import { ShelfIcon } from '../../common-elements';
import { Markdown } from '../Markdown/Markdown';
export interface CallbackTitleProps { export interface CallbackTitleProps {
name: string; name: string;
description?: string;
opened?: boolean; opened?: boolean;
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
@ -11,11 +13,12 @@ export interface CallbackTitleProps {
export class CallbackTitle extends React.PureComponent<CallbackTitleProps> { export class CallbackTitle extends React.PureComponent<CallbackTitleProps> {
render() { render() {
const { name, opened, className, onClick } = this.props; const { name, description, opened, className, onClick } = this.props;
return ( return (
<div className={className} onClick={onClick || undefined}> <div className={className} onClick={onClick || undefined}>
<ShelfIcon size={'1.5em'} direction={opened ? 'up' : 'down'} float={'left'} /> <ShelfIcon size={'1.5em'} direction={opened ? 'down' : 'right'} float={'left'} />
<strong>{name} </strong> <strong>{name} </strong>
{description && <Markdown compact={true} inline={true} source={description} />}
</div> </div>
); );
} }

View File

@ -27,7 +27,9 @@ export class CallbacksList extends React.PureComponent<CallbacksListProps> {
<div> <div>
<CallbacksHeader> Callbacks </CallbacksHeader> <CallbacksHeader> Callbacks </CallbacksHeader>
{callbacks.map(callback => { {callbacks.map(callback => {
return <CallbackView key={callback.name} callback={callback} />; return callback.operations.map(operation => {
return <CallbackView key={callback.name} callbackOperation={operation} />;
});
})} })}
</div> </div>
); );

View File

@ -8,15 +8,6 @@ import { Operation } from '..';
import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements'; import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements';
import { ContentItemModel } from '../../services'; import { ContentItemModel } from '../../services';
import { GroupModel, OperationModel } from '../../services/models'; import { GroupModel, OperationModel } from '../../services/models';
import styled from '../../styled-components';
const CallbacksHeader = styled.h3`
font-size: 18px;
padding: 0 40px;
margin: 3em 0 1.1em;
color: #253137;
font-weight: normal;
`;
@observer @observer
export class ContentItems extends React.Component<{ export class ContentItems extends React.Component<{
@ -28,18 +19,6 @@ export class ContentItems extends React.Component<{
return null; return null;
} }
return items.map(item => { return items.map(item => {
if (item.type === 'operation' && item.callbacks.length > 0) {
return (
<React.Fragment key={item.id}>
<ContentItem item={item} />
<CallbacksHeader>Callbacks</CallbacksHeader>
{item.callbacks.map((callbackIndex, idx) => {
return <ContentItems key={idx} items={callbackIndex.operations} />;
})}
</React.Fragment>
);
}
return <ContentItem key={item.id} item={item} />; return <ContentItem key={item.id} item={item} />;
}); });
} }

View File

@ -15,11 +15,18 @@ import { Parameters } from '../Parameters/Parameters';
import { RequestSamples } from '../RequestSamples/RequestSamples'; import { RequestSamples } from '../RequestSamples/RequestSamples';
import { ResponsesList } from '../Responses/ResponsesList'; import { ResponsesList } from '../Responses/ResponsesList';
import { ResponseSamples } from '../ResponseSamples/ResponseSamples'; import { ResponseSamples } from '../ResponseSamples/ResponseSamples';
import { CallbacksList } from '../Callbacks';
import { CallbackSamples } from '../CallbackSamples/CallbackSamples';
import { OperationModel as OperationType } from '../../services/models'; import { OperationModel as OperationType } from '../../services/models';
import styled from '../../styled-components'; import styled from '../../styled-components';
import { Extensions } from '../Fields/Extensions'; import { Extensions } from '../Fields/Extensions';
const CallbackMiddlePanel = styled(MiddlePanel)`
width: 100%;
padding: 0;
`;
const OperationRow = styled(Row)` const OperationRow = styled(Row)`
backface-visibility: hidden; backface-visibility: hidden;
contain: content; contain: content;
@ -42,18 +49,23 @@ export class Operation extends React.Component<OperationProps> {
const { name: summary, description, deprecated, externalDocs } = operation; const { name: summary, description, deprecated, externalDocs } = operation;
const hasDescription = !!(description || externalDocs); const hasDescription = !!(description || externalDocs);
const AdaptiveMiddlePanel = operation.isCallback ? CallbackMiddlePanel : MiddlePanel;
return ( return (
<OptionsContext.Consumer> <OptionsContext.Consumer>
{options => ( {options => (
<OperationRow> <OperationRow>
<MiddlePanel> <AdaptiveMiddlePanel>
<H2> {!operation.isCallback && (
<ShareLink to={operation.id} /> <H2>
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>} <ShareLink to={operation.id} />
</H2> {summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
{options.pathInMiddlePanel && <Endpoint operation={operation} inverted={true} />} </H2>
{hasDescription && ( )}
{!operation.isCallback && options.pathInMiddlePanel && (
<Endpoint operation={operation} inverted={true} />
)}
{!operation.isCallback && hasDescription && (
<Description> <Description>
{description !== undefined && <Markdown source={description} />} {description !== undefined && <Markdown source={description} />}
{externalDocs && <ExternalDocumentation externalDocs={externalDocs} />} {externalDocs && <ExternalDocumentation externalDocs={externalDocs} />}
@ -63,12 +75,18 @@ export class Operation extends React.Component<OperationProps> {
<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> <CallbacksList callbacks={operation.callbacks} />
<DarkRightPanel> </AdaptiveMiddlePanel>
{!options.pathInMiddlePanel && <Endpoint operation={operation} />} {!operation.isCallback && (
<RequestSamples operation={operation} /> <DarkRightPanel>
<ResponseSamples operation={operation} /> {!options.pathInMiddlePanel && <Endpoint operation={operation} />}
</DarkRightPanel> <RequestSamples operation={operation} />
<ResponseSamples operation={operation} />
{operation.callbacks.length > 0 && (
<CallbackSamples callbacks={operation.callbacks} />
)}
</DarkRightPanel>
)}
</OperationRow> </OperationRow>
)} )}
</OptionsContext.Consumer> </OptionsContext.Consumer>

View File

@ -22,7 +22,7 @@ describe('Components', () => {
options, options,
); );
const callbackViewElement = shallow( const callbackViewElement = shallow(
<CallbackView key={callback.name} callback={callback} />, <CallbackView key={callback.name} callbackOperation={callback.operations[0]} />,
).getElement(); ).getElement();
expect(callbackViewElement.props).toBeDefined(); expect(callbackViewElement.props).toBeDefined();
expect(callbackViewElement.props.children).toBeDefined(); expect(callbackViewElement.props.children).toBeDefined();

View File

@ -172,6 +172,14 @@ export class OperationModel implements IMenuItem {
this.active = false; this.active = false;
} }
/**
* Toggle expansion in middle panel (for callbacks, which are operations)
*/
@action
toggle() {
this.expanded = !this.expanded;
}
expand() { expand() {
if (this.parent) { if (this.parent) {
this.parent.expand(); this.parent.expand();