Skip to content

Commit

Permalink
Merge branch 'main' into fix-tryit-panel
Browse files Browse the repository at this point in the history
  • Loading branch information
mnaumanali94 authored Nov 5, 2024
2 parents 6edeaa4 + c4fc263 commit 6e89828
Show file tree
Hide file tree
Showing 20 changed files with 497 additions and 285 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"lerna": "^7.4.2",
"postcss": "8.4.43",
"postcss-cli": "8.3.1",
"postcss-import": "14.0.2",
"postcss-import": "16.1.0",
"postcss-loader": "8.1.1",
"prettier": "2.8.8",
"process": "0.11.10",
Expand All @@ -68,7 +68,8 @@
"resolutions": {
"fast-xml-parser": "4.4.1",
"tar": "6.2.1",
"micromatch": "4.0.8"
"micromatch": "4.0.8",
"@stoplight/react-error-boundary": "3.0.0"
},
"scripts": {
"demo": "yarn workspace @stoplight/elements-demo",
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": "8.4.3",
"version": "8.4.7",
"sideEffects": [
"web-components.min.js",
"src/web-components/**",
Expand Down
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 @@ -13,6 +13,7 @@ import { SectionSubtitle } from '../Sections';
export interface BodyProps {
body: IHttpOperationRequestBody;
onChange?: (requestBodyIndex: number) => void;
isHttpWebhookOperation?: boolean;
}

export const isBodyEmpty = (body?: BodyProps['body']) => {
Expand All @@ -23,7 +24,7 @@ export const isBodyEmpty = (body?: BodyProps['body']) => {
return contents.length === 0 && !description?.trim();
};

export const Body = ({ body, onChange }: BodyProps) => {
export const Body = ({ body, onChange, isHttpWebhookOperation = false }: BodyProps) => {
const [refResolver, maxRefDepth] = useSchemaInlineRefResolver();
const [chosenContent, setChosenContent] = React.useState(0);
const { nodeHasChanged, renderExtensionAddon } = useOptionsCtx();
Expand Down Expand Up @@ -61,13 +62,12 @@ export const Body = ({ body, onChange }: BodyProps) => {
<NodeAnnotation change={descriptionChanged} />
</Box>
)}

{isJSONSchema(schema) && (
<JsonSchemaViewer
resolveRef={refResolver}
maxRefDepth={maxRefDepth}
schema={getOriginalObject(schema)}
viewMode="write"
viewMode={isHttpWebhookOperation ? 'standalone' : 'write'}
renderRootTreeLines
nodeHasChanged={nodeHasChanged}
renderExtensionAddon={renderExtensionAddon}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,13 @@ const HttpOperationComponent = React.memo<HttpOperationProps>(
<NodeAnnotation change={descriptionChanged} />
</Box>
)}

<NodeVendorExtensions data={data} />

<Request
onChange={setTextRequestBodyIndex}
operation={data}
hideSecurityInfo={layoutOptions?.hideSecurityInfo}
isHttpWebhookOperation={isHttpWebhookOperation(data)}
/>

{data.responses && (
<Responses
responses={data.responses}
Expand All @@ -113,9 +111,7 @@ const HttpOperationComponent = React.memo<HttpOperationProps>(
isCompact={isCompact}
/>
)}

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

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

const pathElem = (
<Flex overflowX="hidden" fontSize="lg" userSelect="all">
<Box dir="rtl" color="muted" textOverflow="truncate" overflowX="hidden">
<Box as="span" dir="ltr" style={{ unicodeBidi: 'bidi-override' }}>
<Box dir="rtl" textOverflow="truncate" overflowX="hidden">
<Box as="span" dir="ltr" color="muted" style={{ unicodeBidi: 'bidi-override' }}>
{chosenServerUrl}
</Box>
</Box>
<Box fontWeight="semibold" flex={1}>
{path}
<Box as="span" fontWeight="semibold" flex={1}>
{path}
</Box>
</Box>
</Flex>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface IRequestProps {
operation: IHttpEndpointOperation;
hideSecurityInfo?: boolean;
onChange?: (requestBodyIndex: number) => void;
isHttpWebhookOperation?: boolean;
}

export const Request: React.FunctionComponent<IRequestProps> = ({
Expand All @@ -33,6 +34,7 @@ export const Request: React.FunctionComponent<IRequestProps> = ({
},
hideSecurityInfo,
onChange,
isHttpWebhookOperation = false,
}) => {
if (!request || typeof request !== 'object') return null;

Expand Down Expand Up @@ -82,7 +84,7 @@ export const Request: React.FunctionComponent<IRequestProps> = ({
</VStack>
)}

{body && <Body onChange={onChange} body={body} />}
{body && <Body onChange={onChange} body={body} isHttpWebhookOperation={isHttpWebhookOperation} />}
</VStack>
);
};
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,14 +24,23 @@ 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})`
: license?.identifier && licenseUrl
? `[${license?.identifier}](${licenseUrl})`
: undefined;
: '';
const tosLink = termsOfService ? `[Terms of Service](${termsOfService})` : '';

return contactLink || licenseLink || tosLink ? (
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import { useGenerateExampleFromMediaTypeContent } from '../../../utils/exampleGe

export const useTextRequestBodyState = (
mediaTypeContent: IMediaTypeContent | undefined,
skipReadOnly: boolean,
): [string, React.Dispatch<React.SetStateAction<string>>] => {
const initialRequestBody = useGenerateExampleFromMediaTypeContent(mediaTypeContent, undefined, {
skipReadOnly: true,
skipReadOnly,
});

const [textRequestBody, setTextRequestBody] = React.useState<string>(initialRequestBody);
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
5 changes: 4 additions & 1 deletion packages/elements-core/src/components/TryIt/TryIt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ export const TryIt: React.FC<TryItProps> = ({
const [bodyParameterValues, setBodyParameterValues, isAllowedEmptyValues, setAllowedEmptyValues, formDataState] =
useBodyParameterState(mediaTypeContent);

const [textRequestBody, setTextRequestBody] = useTextRequestBodyState(mediaTypeContent);
const [textRequestBody, setTextRequestBody] = useTextRequestBodyState(
mediaTypeContent,
!isHttpWebhookOperation(httpOperation),
);

const [operationAuthValue, setOperationAuthValue, setCurrentScheme] = usePersistedSecuritySchemeWithValues();

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
Loading

0 comments on commit 6e89828

Please sign in to comment.