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,18 +530,19 @@ paths:
example: AAA-123-BBB-456
callbacks:
orderInProgress:
'/{$request.body#/callbackUrl}?event={$request.body#/eventName}':
'{$request.body#/callbackUrl}?event={$request.body#/eventName}':
servers:
- url: //petstore.swagger.io/v2
description: Default server callback
- url: //petstore.swagger.io/sandbox
description: Sandbox server callback
- url: //callback-url.path-level/v1
description: Path level server 1
- url: //callback-url.path-level/v2
description: Path level server 2
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:
content:
application/json:
schema:
schema:
type: object
properties:
orderId:
@ -613,7 +614,12 @@ paths:
var_dump($e->getErrors());
}
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:
content:
application/json:
@ -656,9 +662,16 @@ paths:
type: string
example: '123'
orderShipped:
'/{$request.body#/callbackUrl}?event={$request.body#/eventName}':
'{$request.body#/callbackUrl}?event={$request.body#/eventName}':
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:
content:
application/json:
@ -680,8 +693,10 @@ paths:
'200':
description: Callback successfully processed and no retries will be performed
orderDelivered:
'/{$request.body#/callbackUrl}?event={$request.body#/eventName}':
'http://notificationServer.com?url={$request.body#/callbackUrl}&event={$request.body#/eventName}':
post:
deprecated: true
summary: Order delivered
description: A callback triggered every time an Order is delivered to the recipient
requestBody:
content:

View File

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

View File

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

View File

@ -1,25 +1,46 @@
import * as React from 'react';
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 {
name: string;
description?: string;
opened?: boolean;
className?: string;
onClick?: () => void;
httpVerb: string;
deprecated?: boolean;
}
export class CallbackTitle extends React.PureComponent<CallbackTitleProps> {
render() {
const { name, description, opened, className, onClick } = this.props;
const { name, opened, className, onClick, httpVerb, deprecated } = this.props;
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'} />
<strong>{name} </strong>
{description && <Markdown compact={true} inline={true} source={description} />}
</div>
<CallbackNameStyled deprecated={deprecated}>{name}</CallbackNameStyled>
{deprecated ? <Badge type="warning"> {l('deprecated')} </Badge> : null}
</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) => {
return {
label: `[${callback.httpVerb.toUpperCase()}] ${callback.name}`,
label: `${callback.httpVerb.toUpperCase()}: ${callback.name}`,
value: idx.toString(),
};
});

View File

@ -21,6 +21,7 @@ export interface EndpointProps {
hideHostname?: boolean;
inverted?: boolean;
compact?: boolean;
}
export interface EndpointState {
@ -49,7 +50,9 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
{options => (
<OperationEndpointWrap>
<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>
<ShelfIcon
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}`,
}))<{ type: string }>`
font-size: 0.929em;
line-height: 20px;
background-color: ${(props: any) => props.theme.colors.http[props.type] || '#999999'};
}))<{ type: string; compact?: boolean }>`
font-size: ${props => (props.compact ? '0.8em' : '0.929em')};
line-height: ${props => (props.compact ? '18px' : '20px')};
background-color: ${props => props.theme.colors.http[props.type] || '#999999'};
color: #ffffff;
padding: 3px 10px;
padding: ${props => (props.compact ? '2px 8px' : '3px 10px')};
text-transform: uppercase;
font-family: ${props => props.theme.typography.headings.fontFamily};
margin: 0;
@ -59,7 +59,6 @@ export const ServersOverlay = styled.div<{ expanded: boolean }>`
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
transition: all 0.25s ease;
${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 { ShareLink } from '../../common-elements/linkify';
import { OperationModel as OperationType } from '../../services/models';
import styled from '../../styled-components';
import styled, { media } from '../../styled-components';
import { CallbacksList } from '../Callbacks';
import { CallbackSamples } from '../CallbackSamples/CallbackSamples';
import { Endpoint } from '../Endpoint/Endpoint';
@ -18,22 +18,6 @@ import { ResponsesList } from '../Responses/ResponsesList';
import { ResponseSamples } from '../ResponseSamples/ResponseSamples';
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 {
operation: OperationType;
}
@ -70,7 +54,7 @@ export class Operation extends React.Component<OperationProps> {
<Extensions extensions={operation.extensions} />
<SecurityRequirements securities={operation.security} />
<Parameters parameters={operation.parameters} body={operation.requestBody} />
<ResponsesList responses={operation.responses} />
<ResponsesList responses={operation.responses} isCallback={operation.isCallback} />
<CallbacksList callbacks={operation.callbacks} />
</AdaptiveMiddlePanel>
{!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 {
responses: ResponseModel[];
isCallback?: boolean;
}
export class ResponsesList extends React.PureComponent<ResponseListProps> {
render() {
const { responses } = this.props;
const { responses, isCallback } = this.props;
if (!responses || responses.length === 0) {
return null;
@ -25,7 +26,7 @@ export class ResponsesList extends React.PureComponent<ResponseListProps> {
return (
<div>
<ResponsesHeader> Responses </ResponsesHeader>
<ResponsesHeader>{isCallback ? 'Callback responses' : 'Responses'}</ResponsesHeader>
{responses.map(response => {
return <ResponseView key={response.code} response={response} />;
})}

View File

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

View File

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

View File

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