Update callbacks layout

This commit is contained in:
Oleksiy Kachynskyy 2020-04-02 10:25:13 +03:00
parent b1ef91379a
commit aa5d6c0c8e
12 changed files with 113 additions and 57 deletions

View File

@ -530,14 +530,15 @@ paths:
example: AAA-123-BBB-456 example: AAA-123-BBB-456
callbacks: callbacks:
orderInProgress: orderInProgress:
'/{$request.body#/callbackUrl}?event={$request.body#/eventName}': '{$request.body#/callbackUrl}?event={$request.body#/eventName}':
servers: servers:
- url: //petstore.swagger.io/v2 - url: //callback-url.path-level/v1
description: Default server callback description: Path level server 1
- url: //petstore.swagger.io/sandbox - url: //callback-url.path-level/v2
description: Sandbox server callback description: Path level server 2
post: post:
description: A callback triggered every time an Order is updated status to "inProgress" summary: Order in Progress (Summary)
description: A callback triggered every time an Order is updated status to "inProgress" (Description)
requestBody: requestBody:
content: content:
application/json: application/json:
@ -613,7 +614,12 @@ paths:
var_dump($e->getErrors()); var_dump($e->getErrors());
} }
put: put:
description: A Put callback triggered description: Order in Progress (Only Description)
servers:
- url: //callback-url.operation-level/v1
description: Operation level server 1 (Path override)
- url: //callback-url.operation-level/v2
description: Operation level server 2 (Path override)
requestBody: requestBody:
content: content:
application/json: application/json:
@ -656,9 +662,16 @@ paths:
type: string type: string
example: '123' example: '123'
orderShipped: orderShipped:
'/{$request.body#/callbackUrl}?event={$request.body#/eventName}': '{$request.body#/callbackUrl}?event={$request.body#/eventName}':
post: post:
description: A callback triggered every time an Order is shipped description: |
Very long description
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.
requestBody: requestBody:
content: content:
application/json: application/json:
@ -680,8 +693,10 @@ paths:
'200': '200':
description: Callback successfully processed and no retries will be performed description: Callback successfully processed and no retries will be performed
orderDelivered: orderDelivered:
'/{$request.body#/callbackUrl}?event={$request.body#/eventName}': 'http://notificationServer.com?url={$request.body#/callbackUrl}&event={$request.body#/eventName}':
post: post:
deprecated: true
summary: Order delivered
description: A callback triggered every time an Order is delivered to the recipient description: A callback triggered every time an Order is delivered to the recipient
requestBody: requestBody:
content: content:

View File

@ -6,21 +6,23 @@ import { CallbackDetailsWrap, StyledCallbackTitle } from '../Callbacks/styled.el
import { CallbackDetails } from './CallbackDetails'; import { CallbackDetails } from './CallbackDetails';
@observer @observer
// TODO: rename to Callback
export class CallbackView extends React.Component<{ callbackOperation: OperationModel }> { export class CallbackView extends React.Component<{ callbackOperation: OperationModel }> {
toggle = () => { toggle = () => {
this.props.callbackOperation.toggle(); this.props.callbackOperation.toggle();
}; };
render() { render() {
const { name, description, expanded } = this.props.callbackOperation; const { name, expanded, httpVerb, deprecated } = this.props.callbackOperation;
return ( return (
<> <>
<StyledCallbackTitle <StyledCallbackTitle
onClick={this.toggle} onClick={this.toggle}
name={name} name={name}
description={description}
opened={expanded} opened={expanded}
httpVerb={httpVerb}
deprecated={deprecated}
/> />
{expanded && ( {expanded && (
<CallbackDetailsWrap> <CallbackDetailsWrap>

View File

@ -3,14 +3,28 @@ import * as React from 'react';
import { OperationModel } from '../../services/models'; import { OperationModel } from '../../services/models';
import { OperationItem } from '../ContentItems/ContentItems'; import { OperationItem } from '../ContentItems/ContentItems';
import { Endpoint } from '../Endpoint/Endpoint'; import { Endpoint } from '../Endpoint/Endpoint';
import { Markdown } from '../Markdown/Markdown';
import styled from '../../styled-components';
export class CallbackDetails extends React.PureComponent<{ callbackOperation: OperationModel }> { export class CallbackDetails extends React.PureComponent<{ callbackOperation: OperationModel }> {
render() { render() {
const description = this.props.callbackOperation.description;
return ( return (
<div> <div>
<Endpoint operation={this.props.callbackOperation} inverted={true} /> {description && (
<CallbackDescription>
<Markdown compact={true} inline={true} source={description} />
</CallbackDescription>
)}
<Endpoint operation={this.props.callbackOperation} inverted={true} compact={true} />
<OperationItem item={this.props.callbackOperation} /> <OperationItem item={this.props.callbackOperation} />
</div> </div>
); );
} }
} }
const CallbackDescription = styled.div`
margin-top: 10px;
margin-bottom: 20px;
`;

View File

@ -1,25 +1,46 @@
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'; import { OperationBadge } from '../SideMenu/styled.elements';
import { shortenHTTPVerb } from '../../utils/openapi';
import styled from '../../styled-components';
import { Badge } from '../../common-elements/';
import { l } from '../../services/Labels';
export interface CallbackTitleProps { export interface CallbackTitleProps {
name: string; name: string;
description?: string;
opened?: boolean; opened?: boolean;
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
httpVerb: string;
deprecated?: boolean;
} }
export class CallbackTitle extends React.PureComponent<CallbackTitleProps> { export class CallbackTitle extends React.PureComponent<CallbackTitleProps> {
render() { render() {
const { name, description, opened, className, onClick } = this.props; const { name, opened, className, onClick, httpVerb, deprecated } = this.props;
return ( return (
<div className={className} onClick={onClick || undefined}> <CallbackTitleWrapper className={className} onClick={onClick || undefined}>
<OperationBadgeStyled type={httpVerb}>{shortenHTTPVerb(httpVerb)}</OperationBadgeStyled>
<ShelfIcon size={'1.5em'} direction={opened ? 'down' : 'right'} float={'left'} /> <ShelfIcon size={'1.5em'} direction={opened ? 'down' : 'right'} float={'left'} />
<strong>{name} </strong> <CallbackNameStyled deprecated={deprecated}>{name}</CallbackNameStyled>
{description && <Markdown compact={true} inline={true} source={description} />} {deprecated ? <Badge type="warning"> {l('deprecated')} </Badge> : null}
</div> </CallbackTitleWrapper>
); );
} }
} }
const CallbackTitleWrapper = styled.div`
& > * {
vertical-align: middle;
}
`;
const CallbackNameStyled = styled.span<{ deprecated?: boolean }>`
text-decoration: ${props => (props.deprecated ? 'line-through' : 'none')};
margin-right: 8px;
`;
const OperationBadgeStyled = styled(OperationBadge)`
margin: 0px 5px 0px 0px;
`;

View File

@ -43,7 +43,7 @@ export class CallbacksSwitch extends React.Component<CallbacksSwitchProps, Callb
const options = callbacks.map((callback, idx) => { const options = callbacks.map((callback, idx) => {
return { return {
label: `[${callback.httpVerb.toUpperCase()}] ${callback.name}`, label: `${callback.httpVerb.toUpperCase()}: ${callback.name}`,
value: idx.toString(), value: idx.toString(),
}; };
}); });

View File

@ -21,6 +21,7 @@ export interface EndpointProps {
hideHostname?: boolean; hideHostname?: boolean;
inverted?: boolean; inverted?: boolean;
compact?: boolean;
} }
export interface EndpointState { export interface EndpointState {
@ -49,7 +50,9 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
{options => ( {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} compact={this.props.compact}>
{operation.httpVerb}
</HttpVerb>
<ServerRelativeURL>{operation.path}</ServerRelativeURL> <ServerRelativeURL>{operation.path}</ServerRelativeURL>
<ShelfIcon <ShelfIcon
float={'right'} float={'right'}

View File

@ -34,14 +34,14 @@ export const EndpointInfo = styled.div<{ expanded?: boolean; inverted?: boolean
} }
`; `;
export const HttpVerb = styled.span.attrs((props: { type: string }) => ({ export const HttpVerb = styled.span.attrs((props: { type: string; compact?: boolean }) => ({
className: `http-verb ${props.type}`, className: `http-verb ${props.type}`,
}))<{ type: string }>` }))<{ type: string; compact?: boolean }>`
font-size: 0.929em; font-size: ${props => (props.compact ? '0.8em' : '0.929em')};
line-height: 20px; line-height: ${props => (props.compact ? '18px' : '20px')};
background-color: ${(props: any) => props.theme.colors.http[props.type] || '#999999'}; background-color: ${props => props.theme.colors.http[props.type] || '#999999'};
color: #ffffff; color: #ffffff;
padding: 3px 10px; padding: ${props => (props.compact ? '2px 8px' : '3px 10px')};
text-transform: uppercase; text-transform: uppercase;
font-family: ${props => props.theme.typography.headings.fontFamily}; font-family: ${props => props.theme.typography.headings.fontFamily};
margin: 0; margin: 0;
@ -59,7 +59,6 @@ export const ServersOverlay = styled.div<{ expanded: boolean }>`
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
transition: all 0.25s ease; transition: all 0.25s ease;
${props => (props.expanded ? '' : 'transform: translateY(-50%) scaleY(0);')} ${props => (props.expanded ? '' : 'transform: translateY(-50%) scaleY(0);')}
`; `;

View File

@ -4,7 +4,7 @@ import * as React from 'react';
import { Badge, DarkRightPanel, H2, MiddlePanel, Row } from '../../common-elements'; import { Badge, DarkRightPanel, H2, MiddlePanel, Row } from '../../common-elements';
import { ShareLink } from '../../common-elements/linkify'; import { ShareLink } from '../../common-elements/linkify';
import { OperationModel as OperationType } from '../../services/models'; import { OperationModel as OperationType } from '../../services/models';
import styled from '../../styled-components'; import styled, { media } from '../../styled-components';
import { CallbacksList } from '../Callbacks'; import { CallbacksList } from '../Callbacks';
import { CallbackSamples } from '../CallbackSamples/CallbackSamples'; import { CallbackSamples } from '../CallbackSamples/CallbackSamples';
import { Endpoint } from '../Endpoint/Endpoint'; import { Endpoint } from '../Endpoint/Endpoint';
@ -18,22 +18,6 @@ import { ResponsesList } from '../Responses/ResponsesList';
import { ResponseSamples } from '../ResponseSamples/ResponseSamples'; import { ResponseSamples } from '../ResponseSamples/ResponseSamples';
import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement'; import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement';
const CallbackMiddlePanel = styled(MiddlePanel)`
width: 100%;
padding: 0;
`;
const OperationRow = styled(Row)`
backface-visibility: hidden;
contain: content;
overflow: hidden;
`;
const Description = styled.div`
margin-bottom: ${({ theme }) => theme.spacing.unit * 6}px;
`;
export interface OperationProps { export interface OperationProps {
operation: OperationType; operation: OperationType;
} }
@ -70,7 +54,7 @@ export class Operation extends React.Component<OperationProps> {
<Extensions extensions={operation.extensions} /> <Extensions extensions={operation.extensions} />
<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} isCallback={operation.isCallback} />
<CallbacksList callbacks={operation.callbacks} /> <CallbacksList callbacks={operation.callbacks} />
</AdaptiveMiddlePanel> </AdaptiveMiddlePanel>
{!operation.isCallback && ( {!operation.isCallback && (
@ -89,3 +73,22 @@ export class Operation extends React.Component<OperationProps> {
); );
} }
} }
const CallbackMiddlePanel = styled(MiddlePanel)`
width: 100%;
padding: 0;
${() => media.lessThan('medium', true)`
padding: 0
`};
`;
const OperationRow = styled(Row)`
backface-visibility: hidden;
contain: content;
overflow: hidden;
`;
const Description = styled.div`
margin-bottom: ${({ theme }) => theme.spacing.unit * 6}px;
`;

View File

@ -13,11 +13,12 @@ const ResponsesHeader = styled.h3`
export interface ResponseListProps { export interface ResponseListProps {
responses: ResponseModel[]; responses: ResponseModel[];
isCallback?: boolean;
} }
export class ResponsesList extends React.PureComponent<ResponseListProps> { export class ResponsesList extends React.PureComponent<ResponseListProps> {
render() { render() {
const { responses } = this.props; const { responses, isCallback } = this.props;
if (!responses || responses.length === 0) { if (!responses || responses.length === 0) {
return null; return null;
@ -25,7 +26,7 @@ export class ResponsesList extends React.PureComponent<ResponseListProps> {
return ( return (
<div> <div>
<ResponsesHeader> Responses </ResponsesHeader> <ResponsesHeader>{isCallback ? 'Callback responses' : 'Responses'}</ResponsesHeader>
{responses.map(response => { {responses.map(response => {
return <ResponseView key={response.code} response={response} />; return <ResponseView key={response.code} response={response} />;
})} })}

View File

@ -31,7 +31,7 @@ describe('Components', () => {
it('should correctly render CallbackTitle', () => { it('should correctly render CallbackTitle', () => {
const callbackTitleViewElement = shallow( const callbackTitleViewElement = shallow(
<CallbackTitle name={'Test'} className={'.test'} onClick={undefined} />, <CallbackTitle name={'Test'} className={'.test'} onClick={undefined} httpVerb={'get'} />,
).getElement(); ).getElement();
expect(callbackTitleViewElement.props).toBeDefined(); expect(callbackTitleViewElement.props).toBeDefined();
expect(callbackTitleViewElement.props.className).toEqual('.test'); expect(callbackTitleViewElement.props.className).toEqual('.test');

View File

@ -39,7 +39,6 @@ export class CallbackModel {
undefined, undefined,
options, options,
true, true,
this.name,
); );
this.operations.push(operation); this.operations.push(operation);

View File

@ -86,7 +86,6 @@ export class OperationModel implements IMenuItem {
parent: GroupModel | undefined, parent: GroupModel | undefined,
private options: RedocNormalizedOptions, private options: RedocNormalizedOptions,
isCallback: boolean = false, isCallback: boolean = false,
callbackEventName?: string,
) { ) {
this.pointer = JsonPointer.compile(['paths', operationSpec.pathName, operationSpec.httpVerb]); this.pointer = JsonPointer.compile(['paths', operationSpec.pathName, operationSpec.httpVerb]);
@ -112,22 +111,21 @@ export class OperationModel implements IMenuItem {
JsonPointer.compile(['paths', operationSpec.pathName]), JsonPointer.compile(['paths', operationSpec.pathName]),
); );
this.name = getOperationSummary(operationSpec);
if (this.isCallback) { if (this.isCallback) {
// NOTE: Use callback's event name as the view label, not the operationID.
this.name = callbackEventName || getOperationSummary(operationSpec);
// NOTE: Callbacks by default should not inherit the specification's global `security` definition. // NOTE: Callbacks by default should not inherit the specification's global `security` definition.
// Can be defined individually per-callback in the specification. Defaults to none. // Can be defined individually per-callback in the specification. Defaults to none.
this.security = (operationSpec.security || []).map( this.security = (operationSpec.security || []).map(
security => new SecurityRequirementModel(security, parser), security => new SecurityRequirementModel(security, parser),
); );
// TODO: update getting pathInfo // TODO: update getting pathInfo for overriding servers on path level
this.servers = normalizeServers( this.servers = normalizeServers(
'', '',
operationSpec.servers || (pathInfo && pathInfo.servers) || [], operationSpec.servers || (pathInfo && pathInfo.servers) || [],
); );
} else { } else {
this.name = getOperationSummary(operationSpec);
this.security = (operationSpec.security || parser.spec.security || []).map( this.security = (operationSpec.security || parser.spec.security || []).map(
security => new SecurityRequirementModel(security, parser), security => new SecurityRequirementModel(security, parser),
); );
@ -256,6 +254,7 @@ export class OperationModel implements IMenuItem {
@memoize @memoize
get callbacks() { get callbacks() {
console.log('this.operationSpec.callbacks', this.operationSpec.callbacks);
return Object.keys(this.operationSpec.callbacks || []).map(callbackEventName => { return Object.keys(this.operationSpec.callbacks || []).map(callbackEventName => {
return new CallbackModel( return new CallbackModel(
this.parser, this.parser,