Skip to content

Commit

Permalink
make body field a Code editor field (#1221)
Browse files Browse the repository at this point in the history
body inout
  • Loading branch information
cescoferraro authored Sep 20, 2022
1 parent 3308fae commit c3250b2
Show file tree
Hide file tree
Showing 13 changed files with 310 additions and 132 deletions.
1 change: 1 addition & 0 deletions web/cypress/e2e/Home/CreateTest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe('Create test', () => {
cy.navigateToTestCreationPage();
cy.fillCreateFormBasicStep(name);
cy.setCreateFormUrl('POST', 'http://demo-pokemon-api.demo.svc.cluster.local/pokemon');
cy.get('[data-cy=bodyMode-json]').click();
cy.get('[data-cy=body]').type(
'{"name":"meowth","type":"normal","imageUrl":"https://assets.pokemon.com/assets/cms2/img/pokedex/full/052.png","isFeatured":true}',
{
Expand Down
215 changes: 101 additions & 114 deletions web/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"antd": "4.20.4",
"d3-dag": "0.9.1",
"date-fns": "2.28.0",
"fast-xml-parser": "4.0.10",
"html-react-parser": "3.0.4",
"jmespath": "0.16.0",
"lodash": "4.17.21",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Col, Form, Input, Row} from 'antd';
import {BodyField} from 'components/CreateTestPlugins/Rest/steps/RequestDetails/BodyField/BodyField';
import {IPostmanValues, TDraftTestForm} from 'types/Test.types';
import RequestDetailsAuthInput from '../../../Rest/steps/RequestDetails/RequestDetailsAuthInput/RequestDetailsAuthInput';
import RequestDetailsHeadersInput from '../../../Rest/steps/RequestDetails/RequestDetailsHeadersInput';
Expand Down Expand Up @@ -32,9 +33,7 @@ const UploadCollectionForm = ({form}: IProps) => {
<RequestDetailsUrlInput />
</Col>
<Col span={12}>
<Form.Item className="input-body" data-cy="body" label="Request body" name="body" style={{marginBottom: 0}}>
<Input.TextArea placeholder="Enter request body text" />
</Form.Item>
<BodyField />
</Col>
</Row>
<Row gutter={12}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import CodeMirror from '@uiw/react-codemirror';
import {Form, Radio} from 'antd';
import {BodyFieldContainer} from './BodyFieldContainer';
import {SingleLabel} from './SingleLabel';
import {useBodyMode} from './useBodyMode';
import {useLanguageExtensionsMemo} from './useLanguageExtensionsMemo';

interface IProps {
isEditing?: boolean;
body?: string;
}

export const BodyField = ({isEditing = false, body}: IProps): React.ReactElement => {
const [bodyMode, setBodyMode] = useBodyMode(isEditing, body);
const extensions = useLanguageExtensionsMemo(bodyMode);
const hasNoBody = bodyMode === 'none';
return (
<>
<span>
<SingleLabel label="Request body">{null}</SingleLabel>
<Radio.Group value={bodyMode} onChange={e => setBodyMode(e.target.value)}>
<Radio value="none" data-cy="bodyMode-none">
None
</Radio>
<Radio value="raw" data-cy="bodyMode-raw">
Raw
</Radio>
<Radio value="json" data-cy="bodyMode-json">
JSON
</Radio>
<Radio value="xml" data-cy="bodyMode-xml">
XML
</Radio>
</Radio.Group>
</span>
{hasNoBody && (
<div>
<h4>This request has no body {bodyMode}</h4>
</div>
)}
<BodyFieldContainer $isDisplaying={hasNoBody}>
<Form.Item name="body">
<CodeMirror
data-cy="body"
basicSetup={{lineNumbers: true, indentOnInput: true}}
extensions={extensions}
spellCheck={false}
placeholder={`Enter request body text `}
/>
</Form.Item>
</BodyFieldContainer>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import styled from 'styled-components';

export const BodyFieldContainer = styled.div<{$isDisplaying: boolean}>`
width: 100%;
display: ${({$isDisplaying}) => ($isDisplaying ? 'none' : 'unset')};
&& {
.cm-editor {
overflow: hidden;
display: flex;
border-radius: 2px;
font-size: ${({theme}) => theme.size.md};
outline: 1px solid grey;
font-family: SFPro, serif;
}
.cm-line {
padding: 0;
span {
font-family: SFPro, serif;
}
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Form} from 'antd';
import styled from 'styled-components';

export const SingleLabel = styled(Form.Item)`
&& {
.ant-form-item-control-input {
display: none;
}
.ant-form-item-label {
padding: 0 0 8px;
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {Dispatch, SetStateAction, useEffect, useState} from 'react';
import Validator from 'utils/Validator';

export type BodyMode = 'json' | 'xml' | 'raw' | 'none';

function useGuessBodyModeEffect(
isEditing: undefined | boolean,
setBodyMode: Dispatch<SetStateAction<BodyMode>>,
body?: string
) {
const [initialized, setInitialized] = useState(false);
useEffect(() => {
if (!initialized && isEditing && body) {
setInitialized(true);
setBodyMode(Validator.getBodyType(body));
}
}, [isEditing, setBodyMode, body, initialized, setInitialized]);
}

export function useBodyMode(isEditing?: boolean, body?: string): [BodyMode, Dispatch<SetStateAction<BodyMode>>] {
const [bodyMode, setBodyMode] = useState<BodyMode>('none');
useGuessBodyModeEffect(isEditing, setBodyMode, body);
return [bodyMode, setBodyMode];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {json, jsonLanguage, jsonParseLinter} from '@codemirror/lang-json';
import {xml, xmlLanguage} from '@codemirror/lang-xml';
import {LanguageSupport} from '@codemirror/language';
import {Diagnostic, linter, lintGutter} from '@codemirror/lint';
import {EditorView} from '@codemirror/view';
import {XMLValidator} from 'fast-xml-parser';
import {useMemo} from 'react';
import {BodyMode} from './useBodyMode';

export function useLanguageExtensionsMemo(bodyMode: BodyMode): any[] {
return useMemo(() => {
switch (bodyMode) {
case 'xml':
return [
xml(),
linter((view: EditorView): Diagnostic[] => {
const result = XMLValidator.validate(view.state.doc.sliceString(0));
if (result === true) return [];
return [
{
actions: [],
severity: 'error',
source: result.err.code,
message: result.err.msg,
from: result.err.line,
to: result.err.col,
},
];
}),
new LanguageSupport(xmlLanguage),
lintGutter({}),
];
case 'json':
return [
json(),
linter((view: EditorView): Diagnostic[] => jsonParseLinter()(view)),
new LanguageSupport(jsonLanguage),
lintGutter({}),
];
case 'raw':
return [];
default:
return [lintGutter({})];
}
}, [bodyMode]);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {Form} from 'antd';
import {useCallback, useEffect} from 'react';
import {HTTP_METHOD} from 'constants/Common.constants';
import {useCreateTest} from 'providers/CreateTest/CreateTest.provider';
import CreateStepFooter from 'components/CreateTestSteps/CreateTestStepFooter';
import * as Step from 'components/CreateTestPlugins/Step.styled';
import CreateStepFooter from 'components/CreateTestSteps/CreateTestStepFooter';
import {HTTP_METHOD} from 'constants/Common.constants';
import useValidateTestDraft from 'hooks/useValidateTestDraft';
import {useCreateTest} from 'providers/CreateTest/CreateTest.provider';
import {useCallback, useEffect} from 'react';
import {IHttpValues} from 'types/Test.types';
import RequestDetailsForm from './RequestDetailsForm';

Expand All @@ -30,7 +30,7 @@ const RequestDetails = () => {
form.setFieldsValue({url, body, method: method as HTTP_METHOD});

try {
await form.validateFields();
form.validateFields();
setIsValid(true);
} catch (err) {
setIsValid(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {Form, Input} from 'antd';
import {IHttpValues, TDraftTestForm} from 'types/Test.types';
import {Form} from 'antd';
import * as S from 'components/CreateTestPlugins/Default/steps/BasicDetails/BasicDetails.styled';
import RequestDetailsUrlInput from './RequestDetailsUrlInput';
import {IHttpValues, TDraftTestForm} from 'types/Test.types';
import {BodyField} from './BodyField/BodyField';
import RequestDetailsAuthInput from './RequestDetailsAuthInput/RequestDetailsAuthInput';
import RequestDetailsHeadersInput from './RequestDetailsHeadersInput';
import RequestDetailsUrlInput from './RequestDetailsUrlInput';

export const FORM_ID = 'create-test';

Expand All @@ -18,9 +19,7 @@ const RequestDetailsForm = ({form, isEditing = false}: IProps) => {
<RequestDetailsUrlInput />
<RequestDetailsAuthInput form={form} />
<RequestDetailsHeadersInput />
<Form.Item className="input-body" data-cy="body" label="Request body" name="body" style={{marginBottom: 0}}>
<Input.TextArea placeholder="Enter request body text" />
</Form.Item>
<BodyField body={Form.useWatch('body', form)} isEditing={isEditing} />
</S.InputContainer>
);
};
Expand Down
5 changes: 1 addition & 4 deletions web/src/services/Triggers/Http.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ const HttpTriggerService = (): ITriggerService => ({

async validateDraft(draft): Promise<boolean> {
const {url, method} = draft as IHttpValues;

const isValid = Validator.required(url) && Validator.required(method) && Validator.url(url);

return isValid;
return Validator.required(url) && Validator.required(method) && Validator.url(url);
},

getInitialValues(request) {
Expand Down
31 changes: 31 additions & 0 deletions web/src/utils/Validator.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
import {XMLValidator} from 'fast-xml-parser';
import isEmpty from 'lodash/isEmpty';
import {BodyMode} from '../components/CreateTestPlugins/Rest/steps/RequestDetails/BodyField/useBodyMode';

type Value = any;

const Validator = {
required(value: Value) {
return !isEmpty(value);
},

xml(str: Value) {
if (str === '') return true;
const result = XMLValidator.validate(str);
if (result === true) return true;
return !result.err;
},
getBodyType(str?: Value): BodyMode {
if (!str) return 'none';
if (Validator.json(str)) {
return 'json';
}
if (Validator.xml(str)) {
return 'xml';
}
if (str !== '') {
return 'raw';
}
return 'none';
},
json(str: Value) {
if (str === '') return true;
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
},
url(value: Value) {
try {
if (typeof value === 'string') {
Expand Down

0 comments on commit c3250b2

Please sign in to comment.