Skip to content

Commit

Permalink
fix: refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
v8tenko committed Feb 28, 2024
1 parent fbecfd8 commit 687c928
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 96 deletions.
1 change: 1 addition & 0 deletions src/includer/ui/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ function sandbox({
const searchParams = params?.filter((param: Parameter) => param.in === 'query');
const headers = params?.filter((param: Parameter) => param.in === 'header');
let bodyStr: null | string = null;

if (requestBody?.type === 'application/json' || requestBody?.type === 'multipart/form-data') {
bodyStr = JSON.stringify(prepareSampleObject(requestBody?.schema ?? {}), null, 2);
}
Expand Down
131 changes: 80 additions & 51 deletions src/runtime/components/BodyFormData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {Text, TextArea} from '@gravity-ui/uikit';

import {Text as TextEnum} from '../../plugin/constants';
import {OpenJSONSchema} from '../../includer/models';
import { JSONSchema6Definition } from 'json-schema';

import type {Field, Nullable} from '../types';
import {Column} from './Column';
Expand All @@ -29,73 +30,100 @@ export class BodyFormData extends React.Component<Props, State> implements Field
};
}

renderInput(key: string) {
return (
<Column gap={2}>
<Text variant="body-2">{key}:</Text>
<input
type="file"
onChange={(event) => {
this.createOnChange(key)(event.target.files?.[0]);
}}
/>
</Column>
);
}

renderFileInput(key: string) {
return (
<Column gap={2}>
<Text variant="body-2">{key}:</Text>
<FileInputArray onChange={this.createOnChange(key)} />
</Column>
);
}

renderTextArea(key: string, property: OpenJSONSchema) {
const example = JSON.parse(this.props.example ?? '{}');

const exampleValue =
property.type === 'string' ? example[key] : JSON.stringify(example[key], null, 2);

const rows = property.type === 'string' ? 1 : 3;

return (
<Column gap={2}>
<Text variant="body-2">{key}:</Text>
<TextArea
onUpdate={this.createOnChange(key)}
defaultValue={exampleValue}
rows={rows}
/>
</Column>
);
}

renderProperty(key: string, property: JSONSchema6Definition) {
if (typeof property !== 'object') {
return null;
}

if (property.type === 'string' && property.format === 'binary') {
return this.renderInput(key);
}

const {items} = property;

if (property.type === 'array' && typeof items === 'object' && !Array.isArray(items)) {
const {format, type} = items;

if (type === 'string' && format === 'binary') {
return this.renderFileInput(key);
}
// TODO: string array
}

return this.renderTextArea(key, property);
}

render() {
const {properties, type} = this.props.schema ?? {};
const example = JSON.parse(this.props.example ?? '{}');

if (type !== 'object' || !properties || this.props.bodyType !== 'multipart/form-data') {
return null;
}

return <Column gap={10}>
<Text variant="header-1">{TextEnum.BODY_INPUT_LABEL}</Text>
{
Object.keys(properties).map(key => {
const property = properties[key];
if (typeof property === 'object') {
if (
property.type === 'string'
&& property.format === 'binary'
) {
return <Column gap={2}>
<Text variant="body-2">{key}:</Text>
<input type="file" onChange={event => this.createOnChange(key)(event.target.files?.[0])} />
</Column>
}
const {items} = property || {};

if (property.type === 'array' && typeof items === 'object' && !Array.isArray(items)) {
const {format, type} = items;
if (type === 'string' && format === 'binary') {
return <Column gap={2}>
<Text variant="body-2">{key}:</Text>
<FileInputArray onChange={this.createOnChange(key)} />
</Column>
}
// TODO: string array
}

const exampleValue = property.type === 'string'
? example[key]
: JSON.stringify(example[key], null, 2);

const rows = property.type === 'string'
? 1
: 3;

return <Column gap={2}>
<Text variant="body-2">{key}:</Text>
<TextArea
onUpdate={this.createOnChange(key)}
defaultValue={exampleValue}
rows={rows}
/>
</Column>;
}
return null;
})
}
</Column>
const entries = Object.entries(properties);

return (
<Column gap={10}>
<Text variant="header-1">{TextEnum.BODY_INPUT_LABEL}</Text>
{entries.map(([key, property]) => this.renderProperty(key, property))}
</Column>
);
}

createOnChange(fieldName: string) {
return (newValue: string | undefined | File | File[]) => {
if (!newValue) {
this.formValue.delete(fieldName);

return;
}

if (typeof newValue === 'string') {
this.formValue.set(fieldName, newValue);

return;
}

Expand All @@ -104,11 +132,12 @@ export class BodyFormData extends React.Component<Props, State> implements Field
for (const item of newValue) {
this.formValue.append(fieldName, item);
}

return;
}

this.formValue.set(fieldName, newValue);
}
};
}

validate() {
Expand Down
99 changes: 64 additions & 35 deletions src/runtime/components/FileInputArray.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,73 @@ import React, {useCallback, useRef, useState} from 'react';
import {Button} from '@gravity-ui/uikit';
import {Column} from './Column';

export const FileInputArray = ({onChange}: {onChange: (value: File[]) => void}) => {
const isFile = (item: undefined | File): item is File => item !== undefined;

type Props = {
onChange(value: File[]): void;
}

type IndexedFiles = Record<number, File | undefined>;

export const FileInputArray: React.FC<Props> = ({onChange}) => {
const ref = useRef(1);
const [array, setArray] = useState<Array< {id: number; value: undefined | File}>>([{id: 0, value: undefined}]);

const createOnChange = useCallback((idWithChange: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
setArray(prevArray => {
const newArray = prevArray
.map(({id, value}) =>
id === idWithChange
? {id, value: event.target.files?.[0]}
: {id, value}
);
onChange(newArray.map(({value}) => value).filter(isFile));
return newArray;
});
}, [setArray]);
const [inputs, setInputs] = useState<IndexedFiles>({});

const createOnChange = useCallback(
(idWithChange: number): React.ChangeEventHandler<HTMLInputElement> =>
(event) => {
setInputs((oldState) => {
const file = event.target.files?.[0];
const nextState = {...oldState, [idWithChange]: file }

onChange(Object.values(nextState).filter(isFile));

return nextState;
});
},
[setInputs],
);

const onAdd = useCallback(() => {
setArray(prevState => [...prevState, {id: ref.current++, value: undefined}]);
}, [setArray, ref]);

const createOnRemove = useCallback((idForRemove: number) => () => {
setArray(prevArray => {
return prevArray.filter(({id}) => id !== idForRemove);
});
}, [setArray]);

return <Column gap={3}>
{array.map(({id}, index) => {
return <div key={id}>
<input type="file" onChange={createOnChange(index)} />
{Boolean(index) && <Button view="normal" size="s" type="button" onClick={createOnRemove(id)}>-</Button>}
setInputs((prevState) => ({...prevState, [ref.current++]: undefined }));
}, [setInputs, ref]);

const createOnRemove = useCallback(
(idForRemove: number) => () => {
setInputs((OldState) => {
delete OldState[idForRemove];

return OldState;
});
},
[setInputs],
);

return (
<Column gap={3}>
{Object.keys(inputs).map((id, index) => {
return (
<div key={id}>
<input type="file" onChange={createOnChange(index)} />
{Boolean(index) && (
<Button
view="normal"
size="s"
type="button"
onClick={createOnRemove(Number(id))}
>
-
</Button>
)}
</div>
);
})}
<div>
<Button view="normal" size="s" type="button" onClick={onAdd}>
Add file
</Button>
</div>
})}
<div>
<Button view="normal" size="s" type="button" onClick={onAdd}>Add file</Button>
</div>
</Column>
</Column>
);
};

const isFile = (item: undefined | File): item is File => item !== undefined;
15 changes: 12 additions & 3 deletions src/runtime/sandbox.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type {FormState, SandboxProps} from './types';
import React, {useRef, useState} from 'react';
import {Button} from '@gravity-ui/uikit';
import {BodyJson, BodyFormData, Column, Params, Result} from './components';
import {BodyFormData, BodyJson, Column, Params, Result} from './components';

import {Text, yfmSandbox} from '../plugin/constants';
import {collectErrors, collectValues, prepareHeaders, prepareRequest} from './utils';
Expand All @@ -27,7 +27,11 @@ export const Sandbox: React.FC<SandboxProps> = (props) => {
}

const values = collectValues(refs) as FormState;
const {url, headers, body} = prepareRequest((props.host ?? '') + '/' + props.path, values, props.bodyType);
const {url, headers, body} = prepareRequest(
(props.host ?? '') + '/' + props.path,
values,
props.bodyType,
);

setRequest(
fetch(url, {
Expand Down Expand Up @@ -57,7 +61,12 @@ export const Sandbox: React.FC<SandboxProps> = (props) => {
params={preparedHeaders}
/>
<BodyJson ref={refs.bodyJson} value={props.body} bodyType={props.bodyType} />
<BodyFormData ref={refs.bodyFormData} schema={props.schema} example={props.body} bodyType={props.bodyType} />
<BodyFormData
ref={refs.bodyFormData}
schema={props.schema}
example={props.body}
bodyType={props.bodyType}
/>
{request && <Result request={request} />}
<div>
<Button size="l" view="action" type="submit">
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export type FormState = {
path: Record<string, string>;
search: Record<string, string>;
headers: Record<string, string>;
bodyJson: string | undefined;
bodyFormData: FormData | undefined;
bodyJson?: string;
bodyFormData?: FormData;
};

export type ResponseState = {
Expand Down
17 changes: 12 additions & 5 deletions src/runtime/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ export const merge = <T, R>(items: T[], iterator: (item: T) => Record<string, R>
return items.reduce((acc, item) => Object.assign(acc, iterator(item)), {} as Record<string, R>);
};

export const prepareRequest = (urlTemplate: string, {search, headers, path, bodyJson, bodyFormData}: FormState, bodyType?: string) => {
export const prepareRequest = (
urlTemplate: string,
{search, headers, path, bodyJson, bodyFormData}: FormState,
bodyType?: string,
) => {
const requestUrl = Object.entries(path).reduce((acc, [key, value]) => {
return acc.replace(`{${key}}`, encodeURIComponent(value));
}, urlTemplate);
Expand All @@ -20,10 +24,13 @@ export const prepareRequest = (urlTemplate: string, {search, headers, path, body

return {
url,
headers: bodyType === 'application/json' ? {
...headers,
'Content-Type': 'application/json',
} : headers,
headers:
bodyType === 'application/json'
? {
...headers,
'Content-Type': 'application/json',
}
: headers,
// TODO: match request types (www-form-url-encoded should be handled too)
body: prepareBody({bodyFormData, bodyJson, bodyType}),
};
Expand Down

0 comments on commit 687c928

Please sign in to comment.