@@ -659,7 +659,7 @@ export const openApiTestData: OpenAPIV3.Document = { | |||
type: 'object', | |||
properties: { | |||
id: { type: 'integer', format: 'int64', example: 10 }, | |||
name: { type: 'string', example: 'doggie' }, | |||
name: { type: 'string', minLength: 3, maxLength: 100, example: 'doggie' }, | |||
category: { $ref: '#/components/schemas/Category' }, | |||
photoUrls: { | |||
type: 'array', | |||
@@ -675,6 +675,7 @@ export const openApiTestData: OpenAPIV3.Document = { | |||
type: 'string', | |||
description: 'pet status in the store', | |||
enum: ['available', 'pending', 'sold'], | |||
deprecated: true, | |||
}, | |||
}, | |||
xml: { name: 'pet' }, |
@@ -46,6 +46,9 @@ const ui = { | |||
requestHeader: byRole('list', { name: 'api_documentation.v2.request_subheader.header' }).byRole( | |||
'listitem', | |||
), | |||
requestBodyParameter: byRole('list', { | |||
name: 'api_documentation.v2.request_subheader.request_body', | |||
}).byRole('listitem'), | |||
response: byRole('list', { name: 'api_documentation.v2.response_header' }).byRole('listitem'), | |||
}; | |||
@@ -96,6 +99,22 @@ it('should navigate between apis', async () => { | |||
expect(ui.pathParameter.query()).not.toBeInTheDocument(); | |||
expect(ui.requestHeader.query()).not.toBeInTheDocument(); | |||
expect(ui.requestBody.get()).toBeInTheDocument(); | |||
expect(ui.requestBodyParameter.getAll()).toHaveLength(6); | |||
expect(ui.requestBodyParameter.byRole('button').getAt(0)).toHaveAttribute( | |||
'aria-expanded', | |||
'false', | |||
); | |||
await user.click(ui.requestBodyParameter.byRole('button').getAt(0)); | |||
expect(ui.requestBodyParameter.byRole('button').getAt(0)).toHaveAttribute( | |||
'aria-expanded', | |||
'true', | |||
); | |||
expect(ui.requestBodyParameter.getAt(0)).toHaveTextContent('name requiredmax: 100min: 3'); | |||
await user.click(ui.requestBodyParameter.byRole('button').getAt(0)); | |||
expect(ui.requestBodyParameter.byRole('button').getAt(0)).toHaveAttribute( | |||
'aria-expanded', | |||
'false', | |||
); | |||
expect(ui.response.byRole('button').getAt(0)).toHaveAttribute('aria-expanded', 'true'); | |||
expect(ui.response.byRole('button').getAt(2)).toHaveAttribute('aria-expanded', 'false'); | |||
expect(ui.response.getAt(0)).toHaveTextContent('200Successful operation'); | |||
@@ -138,6 +157,10 @@ it('should navigate between apis', async () => { | |||
expect(ui.queryParameter.getAt(1)).not.toHaveTextContent('deprecated'); | |||
await user.click(ui.queryParameter.byRole('button').getAt(1)); | |||
expect(ui.queryParameter.getAt(1)).toHaveTextContent('max: 5min: -1example: 3'); | |||
await user.click(ui.apiSidebarItem.getAt(7)); | |||
expect(await screen.findByText('/api/v3/pet/{petId}/uploadImage')).toBeInTheDocument(); | |||
expect(screen.getByText('no_data')).toBeInTheDocument(); | |||
}); | |||
it('should show About page', async () => { |
@@ -25,6 +25,7 @@ import React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { ExcludeReferences } from '../types'; | |||
import { mapOpenAPISchema } from '../utils'; | |||
import ApiRequestBodyParameters from './ApiRequestParameters'; | |||
import ApiResponseSchema from './ApiResponseSchema'; | |||
interface Props { | |||
@@ -53,15 +54,6 @@ export default function ApiParameters({ data }: Props) { | |||
return ( | |||
<> | |||
<SubTitle>{translate('api_documentation.v2.parameter_header')}</SubTitle> | |||
{!requestBody && !data.parameters?.length && <TextMuted text={translate('no_data')} />} | |||
{requestBody && ( | |||
<div> | |||
<SubHeading> | |||
{translate('api_documentation.v2.request_subheader.request_body')} | |||
</SubHeading> | |||
<ApiResponseSchema content={requestBody} /> | |||
</div> | |||
)} | |||
{Object.entries(groupBy(data.parameters, (p) => p.in)).map(([group, parameters]) => ( | |||
<div key={group}> | |||
<SubHeading id={`api-parameters-${group}`}> | |||
@@ -103,7 +95,7 @@ export default function ApiParameters({ data }: Props) { | |||
text={`${translate('max')}: ${parameter.schema?.maximum}`} | |||
/> | |||
)} | |||
{parameter.schema?.minimum && ( | |||
{typeof parameter.schema?.minimum === 'number' && ( | |||
<TextMuted | |||
className="sw-mt-2 sw-block" | |||
text={`${translate('min')}: ${parameter.schema?.minimum}`} | |||
@@ -127,6 +119,16 @@ export default function ApiParameters({ data }: Props) { | |||
</ul> | |||
</div> | |||
))} | |||
{!requestBody && !data.parameters?.length && <TextMuted text={translate('no_data')} />} | |||
{requestBody && ( | |||
<div> | |||
<SubHeading id="api_documentation.v2.request_subheader.request_body"> | |||
{translate('api_documentation.v2.request_subheader.request_body')} | |||
</SubHeading> | |||
<ApiResponseSchema content={requestBody} /> | |||
<ApiRequestBodyParameters content={requestBody} /> | |||
</div> | |||
)} | |||
</> | |||
); | |||
} |
@@ -0,0 +1,112 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { Accordion, Badge, TextMuted } from 'design-system'; | |||
import { isEmpty } from 'lodash'; | |||
import { OpenAPIV3 } from 'openapi-types'; | |||
import React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { ExcludeReferences } from '../types'; | |||
interface Props { | |||
content?: Exclude<ExcludeReferences<OpenAPIV3.ResponseObject>['content'], undefined>; | |||
} | |||
export default function ApiRequestBodyParameters({ content }: Readonly<Props>) { | |||
const [openParameters, setOpenParameters] = React.useState<string[]>([]); | |||
const toggleParameter = (parameter: string) => { | |||
if (openParameters.includes(parameter)) { | |||
setOpenParameters(openParameters.filter((n) => n !== parameter)); | |||
} else { | |||
setOpenParameters([...openParameters, parameter]); | |||
} | |||
}; | |||
const schema = | |||
content && | |||
(content['application/json']?.schema || content['application/merge-patch+json']?.schema); | |||
if (!schema?.properties || schema?.type !== 'object' || isEmpty(schema?.properties)) { | |||
return null; | |||
} | |||
const parameters = schema.properties; | |||
const required = schema.required ?? []; | |||
const orderedKeys = Object.keys(parameters).sort((a, b) => { | |||
if (required?.includes(a) && !required?.includes(b)) { | |||
return -1; | |||
} | |||
if (!required?.includes(a) && required?.includes(b)) { | |||
return 1; | |||
} | |||
return 0; | |||
}); | |||
return ( | |||
<ul aria-labelledby="api_documentation.v2.request_subheader.request_body"> | |||
{orderedKeys.map((key) => { | |||
return ( | |||
<Accordion | |||
className="sw-mt-2 sw-mb-4" | |||
key={key} | |||
header={ | |||
<div> | |||
{key}{' '} | |||
{schema.required?.includes(key) && ( | |||
<Badge className="sw-ml-2">{translate('required')}</Badge> | |||
)} | |||
{parameters[key].deprecated && ( | |||
<Badge variant="deleted" className="sw-ml-2"> | |||
{translate('deprecated')} | |||
</Badge> | |||
)} | |||
</div> | |||
} | |||
data={key} | |||
onClick={() => toggleParameter(key)} | |||
open={openParameters.includes(key)} | |||
> | |||
<div>{parameters[key].description}</div> | |||
{parameters[key].maxLength && ( | |||
<TextMuted | |||
className="sw-mt-2 sw-block" | |||
text={`${translate('max')}: ${parameters[key].maxLength}`} | |||
/> | |||
)} | |||
{typeof parameters[key].minLength === 'number' && ( | |||
<TextMuted | |||
className="sw-mt-2 sw-block" | |||
text={`${translate('min')}: ${parameters[key].minLength}`} | |||
/> | |||
)} | |||
{parameters[key].default !== undefined && ( | |||
<TextMuted | |||
className="sw-mt-2 sw-block" | |||
text={`${translate('default')}: ${parameters[key].default}`} | |||
/> | |||
)} | |||
</Accordion> | |||
); | |||
})} | |||
</ul> | |||
); | |||
} |