Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/stoplightio/elements into fix/
Browse files Browse the repository at this point in the history
  • Loading branch information
SB-venkatyadavilli committed Nov 7, 2024
2 parents cf005d5 + 589d49b commit 4fdcec0
Show file tree
Hide file tree
Showing 13 changed files with 125 additions and 24 deletions.
4 changes: 2 additions & 2 deletions docs/getting-started/elements/elements-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
- `apiDescriptionDocument` - OpenAPI document, provided as YAML string, JSON string, or JavaScript object.
- `basePath` - Helps when using `router: 'history'` but docs are in a subdirectory like `https://example.com/docs/api`.
- `hideInternal` - Pass `"true"` to filter out any content which has been marked as internal with `x-internal`.
- `hideTryIt` - Pass `true` to hide the [**Try It** feature](https://docs.stoplight.io/docs/platform/ZG9jOjM2OTM3Mjky-try-it).
- `hideTryIt` - Pass `true` to hide the [**Try It** feature](https://docs.stoplight.io/docs/platform/ZG9jOjM2OTM3Mjky-try-it) completely.
- `hideTryItPanel` - Pass `true` to hide the Try It panel while still display the Request Sample, expects `hideTryIt` to be `false`
- `hideSchemas` - Pass `true` to hide the schemas in the Table of Contents, when using the `sidebar` layout.
- `hideExport` - Pass `true` to hide the Export button on overview section of the documentation.
- `tryItCorsProxy` - Pass the URL of a CORS proxy used to send requests to the Try It feature. The provided URL is pre-pended to the URL of an actual request.
Expand All @@ -19,4 +20,3 @@
- `hash` - uses the hash portion of the URL to keep the UI in sync with the URL.
- `memory` - keeps the history of your "URL" in memory (doesn't read or write to the address bar).
- `static` - renders using the StaticRouter which can help render pages on the server.

Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,14 @@ export const httpOperation: IHttpOperation = {
style: HttpParamStyles.Form,
explode: false,
},
{
schema: {
type: 'object',
},
name: 'deep_object',
style: HttpParamStyles.DeepObject,
explode: true,
},
{
schema: {
type: 'boolean',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,14 @@ export const httpOperation: IHttpOperation = {
name: 'pairs',
style: HttpParamStyles.Form,
},
{
id: '?http-query-pagination?',
schema: {
type: 'object',
},
name: 'pagination',
style: HttpParamStyles.DeepObject,
},
{
id: '?http-query-items?',
schema: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const HttpOperationComponent = React.memo<HttpOperationProps>(
responseStatusCode={responseStatusCode}
requestBodyIndex={requestBodyIndex}
hideTryIt={layoutOptions?.hideTryIt}
hideTryItPanel={layoutOptions?.hideTryItPanel}
hideSamples={layoutOptions?.hideSamples}
tryItCredentialsPolicy={tryItCredentialsPolicy}
mockUrl={mocking.hideMocking ? undefined : mocking.mockUrl}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ const meta: Meta<typeof AdditionalInfo> = {
export default meta;
type Story = StoryObj<typeof AdditionalInfo>;

export const LicenseNameAndURL: Story = {
name: 'License Name with URL',
// Story when only the license URL is provided
export const LicenseWithOnlyURL: Story = {
name: 'License with only URL',
args: {
id: 'id',
license: {
Expand All @@ -21,8 +22,9 @@ export const LicenseNameAndURL: Story = {
},
};

export const LicenseNameAndIdentifier: Story = {
name: 'License Name and Identifier',
// Story when only the license identifier is provided
export const LicenseWithOnlyIdentifier: Story = {
name: 'License with only Identifier',
args: {
id: 'id',
license: {
Expand All @@ -32,8 +34,9 @@ export const LicenseNameAndIdentifier: Story = {
},
};

export const LicenseIdentifierAndNameAndUrl: Story = {
name: 'License Identifier, Name and URL',
// Story when both the license URL and identifier are provided (URL should take precedence)
export const LicenseWithURLAndIdentifier: Story = {
name: 'License with URL and Identifier (URL takes precedence)',
args: {
id: 'id',
license: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,17 @@ export const AdditionalInfo: React.FC<AdditionalInfoProps> = ({ id, termsOfServi
: '';

//use spdx to look up url for license identifier if available
const licenseUrl =
license?.url || license?.identifier ? `https://spdx.org/licenses/${license?.identifier}.html` : undefined;
// The licenseUrl is determined based on the mutual exclusivity of the `url` and `identifier` fields.
// If a `license.url` is provided, it takes precedence over the `license.identifier`.
// This is because the OpenAPI specification defines `url` and `identifier` as mutually exclusive fields,
// meaning you should use either one or the other, but not both. If both are provided, the `url` should be used.
// See: https://spec.openapis.org/oas/latest.html#license-object
const licenseUrl = license?.url
? license?.url
: license?.identifier
? `https://spdx.org/licenses/${license?.identifier}.html`
: undefined;

const licenseLink =
license?.name && licenseUrl
? `[${license.name}](${licenseUrl})`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,39 @@ describe('HttpService', () => {
expect(title).toBeInTheDocument();
});

it('should render additional information with SPDX license identifier', () => {
const contact = {
name: 'Developer',
email: '[email protected]',
url: 'https://stoplight.io/contact-us/',
};

const license = {
name: 'MIT License',
identifier: 'MIT',
};

render(
<AdditionalInfo id="a" contact={contact} license={license} termsOfService="https://stoplight.io/terms/" />,
);

const licenseLink = screen.getByText('MIT License');
expect(licenseLink).toHaveAttribute('href', 'https://spdx.org/licenses/MIT.html');
});

it('should prefer license URL over SPDX identifier if both are provided', () => {
const license = {
name: 'MIT License',
url: 'https://opensource.org/licenses/MIT',
identifier: 'MIT',
};

render(<AdditionalInfo id="a" license={license} />);

const licenseLink = screen.getByText('MIT License');
expect(licenseLink).toHaveAttribute('href', 'https://opensource.org/licenses/MIT');
});

it('should not render if contact, license, and terms of service do not exist', () => {
render(<AdditionalInfo id="a" />);

Expand Down
6 changes: 6 additions & 0 deletions packages/elements-core/src/components/TryIt/TryIt.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ describe('TryIt', () => {
'limit*',
'super_duper_long_parameter_name_with_unnecessary_text*',
'completed',
'deep_object',
'default_style_items',
'items',
'items_not_exploded',
Expand Down Expand Up @@ -283,6 +284,9 @@ describe('TryIt', () => {
const pairsField = screen.getByLabelText('pairs');
userEvent.type(pairsField, '{ "nestedKey": "nestedValue" }');

const pagination = screen.getByLabelText('pagination');
userEvent.type(pagination, '{ "first": 50, "after": "cursor" }');

const itemsField = screen.getByLabelText('items');
userEvent.type(itemsField, '["first", "second"]');

Expand Down Expand Up @@ -312,6 +316,8 @@ describe('TryIt', () => {
expect(queryParams.get('optional_value_with_default')).toBeNull();
expect(queryParams.get('nestedKey')).toBe('nestedValue');
expect(queryParams.get('pairs')).toBeNull();
expect(queryParams.get('pagination[first]')).toBe('50');
expect(queryParams.get('pagination[after]')).toBe('cursor');
expect(queryParams.getAll('items')).toEqual(['first', 'second']);
// assert that headers are passed
const headers = new Headers(fetchMock.mock.calls[0][1]!.headers);
Expand Down
8 changes: 7 additions & 1 deletion packages/elements-core/src/components/TryIt/TryIt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export interface TryItProps {
*/
embeddedInMd?: boolean;

/**
* True when TryIt Panel should be hidden
*/
hideTryItPanel?: boolean;

/**
* Fetch credentials policy for TryIt component
* For more information: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
Expand All @@ -78,6 +83,7 @@ export const TryIt: React.FC<TryItProps> = ({
onRequestChange,
requestBodyIndex,
embeddedInMd = false,
hideTryItPanel = false,
tryItCredentialsPolicy,
corsProxy,
}) => {
Expand Down Expand Up @@ -349,7 +355,7 @@ export const TryIt: React.FC<TryItProps> = ({

return (
<Box rounded="lg" overflowY="hidden">
{tryItPanelElem}
{hideTryItPanel ? null : tryItPanelElem}
{requestData && embeddedInMd && (
<RequestSamples request={requestData} customCodeSamples={customCodeSamples} embeddedInMd />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,10 @@ WithVariables.args = {
httpOperation: operationWithUrlVariables,
};
WithVariables.storyName = 'With Server Variables';

export const WithoutTryItPanel = Template.bind({});
WithoutTryItPanel.args = {
hideTryIt: true,
httpOperation: operationWithUrlVariables,
};
WithoutTryItPanel.storyName = 'Without Try It Panel';
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import { ResponseExamples, ResponseExamplesProps } from '../ResponseExamples/Res
import { TryIt, TryItProps } from './TryIt';

export type TryItWithRequestSamplesProps = Omit<TryItProps, 'onRequestChange'> &
ResponseExamplesProps & { hideTryIt?: boolean; hideSamples?: boolean };
ResponseExamplesProps & { hideTryIt?: boolean; hideTryItPanel?: boolean; hideSamples?: boolean };

export const TryItWithRequestSamples: React.FC<TryItWithRequestSamplesProps> = ({
hideTryIt,
hideTryItPanel,
hideSamples,
...props
}) => {
Expand All @@ -20,12 +21,17 @@ export const TryItWithRequestSamples: React.FC<TryItWithRequestSamplesProps> = (

return (
<VStack spacing={6}>
{!hideTryIt && (
{!hideTryIt ? (
<InvertTheme>
<Box>
<TryIt {...props} onRequestChange={setRequestData} />
<TryIt {...props} hideTryItPanel={hideTryItPanel} onRequestChange={setRequestData} />
</Box>
</InvertTheme>
) : (
// The TryIt is responsible for generating the Request Data so it should always be rendered
<>
<TryIt {...props} hideTryItPanel={hideTryIt} onRequestChange={setRequestData} />
</>
)}

{requestData && !hideSamples && <RequestSamples request={requestData} customCodeSamples={customCodeSamples} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('Build Request', () => {
}).toThrowError('JSON array expected');
});

it('Supports form style', () => {
it('Supports form and deepObject style', () => {
const params = getQueryParams({
httpOperation,
parameterValues: {
Expand All @@ -46,6 +46,7 @@ describe('Build Request', () => {
default_style_items: '["first","second"]',
nested: '{"key":"value"}',
nested_not_exploded: '{"key":"value"}',
deep_object: '{"key":"value", "number": 2}',
},
});

Expand All @@ -58,6 +59,8 @@ describe('Build Request', () => {
{ name: 'default_style_items', value: 'second' },
{ name: 'key', value: 'value' },
{ name: 'nested_not_exploded', value: 'key,value' },
{ name: 'deep_object[key]', value: 'value' },
{ name: 'deep_object[number]', value: '2' },
]);
});

Expand Down
29 changes: 20 additions & 9 deletions packages/elements-core/src/components/TryIt/build-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const getQueryParams = ({

const explode = param.explode ?? true;

if (param.schema?.type === 'object' && param.style === 'form' && value) {
if (param.schema?.type === 'object' && value) {
let nested: Dictionary<string, string>;
try {
nested = JSON.parse(value);
Expand All @@ -80,15 +80,26 @@ export const getQueryParams = ({
throw new Error(`Cannot use param value "${value}". JSON object expected.`);
}

if (explode) {
acc.push(...Object.entries(nested).map(([name, value]) => ({ name, value: value.toString() })));
if (param.style === 'form') {
if (explode) {
acc.push(...Object.entries(nested).map(([name, value]) => ({ name, value: value.toString() })));
} else {
acc.push({
name: param.name,
value: Object.entries(nested)
.map(entry => entry.join(','))
.join(','),
});
}
} else if (param.style === 'deepObject') {
acc.push(
...Object.entries(nested).map(([name, value]) => ({
name: `${param.name}[${name}]`,
value: value.toString(),
})),
);
} else {
acc.push({
name: param.name,
value: Object.entries(nested)
.map(entry => entry.join(','))
.join(','),
});
acc.push({ name: param.name, value });
}
} else if (param.schema?.type === 'array' && value) {
let nested: string[];
Expand Down

0 comments on commit 4fdcec0

Please sign in to comment.