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 * 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';
@observer
export class CallbackView extends React.Component<{ callback: CallbackModel }> {
export class CallbackView extends React.Component<{ callbackOperation: OperationModel }> {
toggle = () => {
this.props.callback.toggle();
this.props.callbackOperation.toggle();
};
render() {
const { name, expanded } = this.props.callback;
const { name, description, expanded } = this.props.callbackOperation;
return (
<div>
<StyledCallbackTitle onClick={this.toggle} name={name} opened={expanded} />
<StyledCallbackTitle
onClick={this.toggle}
name={name}
description={description}
opened={expanded}
/>
{expanded && (
<CallbackDetailsWrap>
<span>{name}</span>
<CallbackDetails callbackOperation={this.props.callbackOperation} />
</CallbackDetailsWrap>
)}
</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 { ShelfIcon } from '../../common-elements';
import { Markdown } from '../Markdown/Markdown';
export interface CallbackTitleProps {
name: string;
description?: string;
opened?: boolean;
className?: string;
onClick?: () => void;
@ -11,11 +13,12 @@ export interface CallbackTitleProps {
export class CallbackTitle extends React.PureComponent<CallbackTitleProps> {
render() {
const { name, opened, className, onClick } = this.props;
const { name, description, opened, className, onClick } = this.props;
return (
<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>
{description && <Markdown compact={true} inline={true} source={description} />}
</div>
);
}

View File

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

View File

@ -8,15 +8,6 @@ import { Operation } from '..';
import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements';
import { ContentItemModel } from '../../services';
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
export class ContentItems extends React.Component<{
@ -28,18 +19,6 @@ export class ContentItems extends React.Component<{
return null;
}
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} />;
});
}

View File

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

View File

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

View File

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