Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: callbacks #2480

Merged
merged 3 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@stoplight/elements": "^7.15.3",
"@stoplight/elements": "^7.16.0",
"@stoplight/mosaic": "^1.46.1",
"history": "^5.0.0",
"react": "16.14.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/elements-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stoplight/elements-core",
"version": "7.15.2",
"version": "7.16.0",
"sideEffects": [
"web-components.min.js",
"src/web-components/**",
Expand Down
103 changes: 102 additions & 1 deletion packages/elements-core/src/__fixtures__/operations/put-todos.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HttpParamStyles, IHttpOperation } from '@stoplight/types';
import { HttpOperationSecurityDeclarationTypes, HttpParamStyles, IHttpOperation } from '@stoplight/types';

export const httpOperation: IHttpOperation = {
id: '?http-operation-id?',
Expand Down Expand Up @@ -522,6 +522,107 @@ export const httpOperation: IHttpOperation = {
},
],
},
callbacks: [
{
key: 'newPet',
extensions: {},
id: '3245690b6a7fc',
method: 'post',
path: '{$request.body#/newPetAvailableUrl}',
request: {
body: {
description: 'Callback body description',
contents: [
{
encodings: [],
examples: [],
id: 'abc',
mediaType: 'application/json',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
properties: {
message: {
examples: ['A new pet has arrived'],
type: 'string',
},
},
required: ['message'],
type: 'object',
},
},
],
id: 'abc',
required: true,
},
cookie: [],
headers: [],
path: [],
query: [],
},
responses: [
{
code: '200',
contents: [],
description: 'Your server returns this code if it accepts the callback',
headers: [],
id: 'abc',
},
],
security: [],
securityDeclarationType: HttpOperationSecurityDeclarationTypes.InheritedFromService,
servers: [],
tags: [],
},
{
key: 'returnedPet',
extensions: {},
id: '07041d5723f4a',
method: 'post',
path: '{$request.body#/returnedPetAvailableUrl}',
request: {
body: {
contents: [
{
encodings: [],
examples: [],
id: 'abc',
mediaType: 'application/json',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
properties: {
message: {
examples: ['A pet has been returned'],
type: 'string',
},
},
required: ['message'],
type: 'object',
},
},
],
id: 'abc',
required: true,
},
cookie: [],
headers: [],
path: [],
query: [],
},
responses: [
{
code: '200',
contents: [],
description: 'Your server returns this code if it accepts the callback',
headers: [],
id: 'abc',
},
],
security: [],
securityDeclarationType: HttpOperationSecurityDeclarationTypes.InheritedFromService,
servers: [],
tags: [],
},
],
tags: [
{
id: '?http-tags-todos?',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { SectionSubtitle } from '../Sections';

export interface BodyProps {
body: IHttpOperationRequestBody;
onChange: (requestBodyIndex: number) => void;
onChange?: (requestBodyIndex: number) => void;
}

export const isBodyEmpty = (body?: BodyProps['body']) => {
Expand All @@ -29,7 +29,7 @@ export const Body = ({ body, onChange }: BodyProps) => {
const { nodeHasChanged } = useOptionsCtx();

React.useEffect(() => {
onChange(chosenContent);
onChange?.(chosenContent);
// disabling because we don't want to react on `onChange` change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chosenContent]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Box, Flex, NodeAnnotation, Select, VStack } from '@stoplight/mosaic';
import { IHttpCallbackOperation } from '@stoplight/types';
import * as React from 'react';

import { useOptionsCtx } from '../../../context/Options';
import { MarkdownViewer } from '../../MarkdownViewer';
import { SectionSubtitle, SectionTitle } from '../Sections';
import { OperationHeader } from './HttpOperation';
import { Request } from './Request';
import { Responses } from './Responses';

export interface CallbacksProps {
callbacks: IHttpCallbackOperation[];
isCompact?: boolean;
}

export interface CallbackProps {
data: IHttpCallbackOperation;
isCompact?: boolean;
}

export const Callbacks = ({ callbacks, isCompact }: CallbacksProps) => {
const [selectedCallbackIndex, setSelectedCallbackIndex] = React.useState(0);

const callback = React.useMemo(() => callbacks[selectedCallbackIndex], [callbacks, selectedCallbackIndex]);

return (
<VStack spacing={8}>
<SectionTitle title="Callbacks" isCompact={isCompact}>
{callbacks.length > 0 && (
<Flex flex={1} justify="end">
<Select
aria-label="Callback"
value={String(selectedCallbackIndex)}
onChange={value => setSelectedCallbackIndex(parseInt(String(value), 10))}
options={callbacks.map((c, index) => ({
label: `${c.key} - ${c.path} - ${c.method}`,
value: index,
}))}
size="sm"
/>
</Flex>
)}
</SectionTitle>

{callback && <Callback data={callback} isCompact={isCompact} />}
</VStack>
);
};

Callbacks.displayName = 'HttpOperation.Callbacks';

export const Callback = ({ data, isCompact }: CallbackProps) => {
const { nodeHasChanged } = useOptionsCtx();

const isDeprecated = !!data.deprecated;
const isInternal = !!data.internal;

const descriptionChanged = nodeHasChanged?.({ nodeId: data.id, attr: 'description' });

return (
<VStack spacing={10}>
<Box>
<SectionSubtitle title={data.key} id="callback-key"></SectionSubtitle>
<OperationHeader
id={data.id}
method={data.method}
path={`/${data.path}`}
isDeprecated={isDeprecated}
isInternal={isInternal}
/>
</Box>
{data.description && (
<Box pos="relative">
<MarkdownViewer className="HttpOperation__Description" markdown={data.description} />
<NodeAnnotation change={descriptionChanged} />
</Box>
)}

<Request operation={data} />

{data.responses && <Responses responses={data.responses} isCompact={isCompact} />}
</VStack>
);
};
Callbacks.displayName = 'HttpOperation.Callback';
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ describe('HttpOperation', () => {

expect(serversButton).toHaveTextContent('PR');

expect(screen.queryByText(/{proto}:\/\/x-{pr}.todos-pr.stoplight.io:{port}/)).toBeInTheDocument();
expect(screen.queryAllByText(/{proto}:\/\/x-{pr}.todos-pr.stoplight.io:{port}/)[0]).toBeInTheDocument();
unmount();
});
});
Expand Down Expand Up @@ -617,6 +617,41 @@ describe('HttpOperation', () => {
});
});

describe('Callbacks', () => {
it('should display callback operation', async () => {
const { unmount } = render(<HttpOperation data={{ ...httpOperation, deprecated: false }} />);

//operation name
expect(screen.queryByText('newPet')).toBeInTheDocument();

// operation header
expect(screen.queryByText('/{$request.body#/newPetAvailableUrl}')).toBeInTheDocument();

// operation body
expect(screen.queryByText('Callback body description')).toBeInTheDocument();

// operation response
expect(screen.queryByText('Your server returns this code if it accepts the callback')).toBeInTheDocument();

unmount();
});
it('should display callback selector and switch between events', () => {
const { unmount } = render(<HttpOperation data={{ ...httpOperation, deprecated: false }} />);

const select = screen.getByLabelText('Callback');

expect(select).toHaveTextContent('newPet - {$request.body#/newPetAvailableUrl} - post');

chooseOption(select, 'returnedPet - {$request.body#/returnedPetAvailableUrl} - post');

expect(select).toHaveTextContent('returnedPet - {$request.body#/returnedPetAvailableUrl} - post');

expect(screen.queryByText('returnedPet')).toBeInTheDocument();

unmount();
});
});

describe('Visibility', () => {
it('should hide TryIt', async () => {
const { unmount } = render(<HttpOperation data={httpOperation} layoutOptions={{ hideTryIt: true }} />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { chosenServerAtom, TryItWithRequestSamples } from '../../TryIt';
import { DocsComponentProps } from '..';
import { TwoColumnLayout } from '../TwoColumnLayout';
import { DeprecatedBadge, InternalBadge } from './Badges';
import { Callbacks } from './Callbacks';
import { Request } from './Request';
import { Responses } from './Responses';

Expand Down Expand Up @@ -84,6 +85,8 @@ const HttpOperationComponent = React.memo<HttpOperationProps>(
/>
)}

{data.callbacks?.length && <Callbacks callbacks={data.callbacks} isCompact={isCompact} />}

{isCompact && tryItPanel}
</VStack>
);
Expand Down Expand Up @@ -170,7 +173,7 @@ function MethodPathInner({ method, path, chosenServerUrl }: MethodPathProps & {
);
}

function OperationHeader({
export function OperationHeader({
id,
noHeading,
hasBadges,
Expand All @@ -182,8 +185,8 @@ function OperationHeader({
}: {
id: string;
noHeading?: boolean;
hasBadges: boolean;
name: string;
hasBadges?: boolean;
name?: string;
isDeprecated?: boolean;
isInternal?: boolean;
method: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Parameters } from './Parameters';

interface IRequestProps {
operation: IHttpOperation;
onChange: (requestBodyIndex: number) => void;
onChange?: (requestBodyIndex: number) => void;
}

export const Request: React.FunctionComponent<IRequestProps> = ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ import { Parameters } from './Parameters';

interface ResponseProps {
response: IHttpOperationResponse;
onMediaTypeChange(mediaType: string): void;
onMediaTypeChange?: (mediaType: string) => void;
}

interface ResponsesProps {
responses: IHttpOperationResponse[];
onMediaTypeChange(mediaType: string): void;
onStatusCodeChange(statusCode: string): void;
onMediaTypeChange?: (mediaType: string) => void;
onStatusCodeChange?: (statusCode: string) => void;
isCompact?: boolean;
}

Expand Down Expand Up @@ -70,7 +70,7 @@ export const Responses = ({
);

React.useEffect(() => {
onStatusCodeChange(activeResponseId);
onStatusCodeChange?.(activeResponseId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeResponseId]);

Expand Down Expand Up @@ -172,7 +172,7 @@ const Response = ({ response, onMediaTypeChange }: ResponseProps) => {
const schema = responseContent?.schema;

React.useEffect(() => {
responseContent && onMediaTypeChange(responseContent.mediaType);
responseContent && onMediaTypeChange?.(responseContent.mediaType);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [responseContent]);

Expand Down
4 changes: 2 additions & 2 deletions packages/elements-dev-portal/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stoplight/elements-dev-portal",
"version": "1.18.1",
"version": "1.19.0",
"description": "UI components for composing beautiful developer documentation.",
"keywords": [],
"sideEffects": [
Expand Down Expand Up @@ -64,7 +64,7 @@
]
},
"dependencies": {
"@stoplight/elements-core": "~7.15.1",
"@stoplight/elements-core": "~7.16.0",
"@stoplight/markdown-viewer": "^5.5.0",
"@stoplight/mosaic": "^1.46.1",
"@stoplight/path": "^1.3.2",
Expand Down
Loading