mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-29 07:34:07 +00:00
feat: add WSRequestBodyMode component and language detection
- Introduced a new component for selecting request body modes (JSON, XML, TEXT). - Implemented auto-detection of language for the request body content. - Created a styled wrapper for improved UI presentation.
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
|
||||
.body-mode-selector {
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
padding-left: 1.5rem !important;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.selected-body-mode {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { humanizeRequestBodyMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
|
||||
const WSRequestBodyMode = ({ mode, onModeChange }) => {
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none selected-body-mode">
|
||||
{humanizeRequestBodyMode(mode)} <IconCaretDown className="caret ml-2" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer body-mode-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div className="label-item font-medium">Raw</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('json');
|
||||
}}
|
||||
>
|
||||
JSON
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('xml');
|
||||
}}
|
||||
>
|
||||
XML
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('text');
|
||||
}}
|
||||
>
|
||||
TEXT
|
||||
</div>
|
||||
{/* <div className="label-item font-medium">Other</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('file');
|
||||
}}
|
||||
>
|
||||
File / Binary
|
||||
</div> */}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default WSRequestBodyMode;
|
||||
@@ -4,14 +4,14 @@ import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { generateGrpcSampleMessage, sendWsRequest } from 'utils/network/index';
|
||||
import { IconChevronDown, IconChevronUp, IconPlus, IconRefresh, IconSend, IconTrash, IconWand } from '@tabler/icons';
|
||||
import { IconChevronDown, IconChevronUp, IconPlus, IconTrash, IconWand } from '@tabler/icons';
|
||||
import CodeEditor from 'components/CodeEditor/index';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
import { applyEdits, format } from 'jsonc-parser';
|
||||
import toast from 'react-hot-toast';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import WSRequestBodyMode from './BodyMode/index';
|
||||
import { autoDetectLang } from 'utils/codemirror/lang-detect';
|
||||
|
||||
const SingleWSMessage = ({
|
||||
message,
|
||||
@@ -25,14 +25,12 @@ const SingleWSMessage = ({
|
||||
canClientSendMultipleMessages
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { displayedTheme, theme } = useTheme();
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
const isConnectionActive = useSelector((state) => state.collections.activeConnections.includes(item.uid));
|
||||
|
||||
const canClientStream = methodType === 'client-streaming' || methodType === 'bidi-streaming';
|
||||
|
||||
const { name, content } = message;
|
||||
const [messageFormat, setMessageFormat] = useState(autoDetectLang(content));
|
||||
|
||||
const onEdit = (value) => {
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
@@ -92,6 +90,12 @@ const SingleWSMessage = ({
|
||||
const getContainerHeight =
|
||||
canClientSendMultipleMessages && body.ws.length > 1 ? `${isCollapsed ? '' : 'h-80'}` : 'h-full';
|
||||
|
||||
const codemirrorMode = {
|
||||
text: 'application/text',
|
||||
xml: 'application/xml',
|
||||
json: 'application/ld+json'
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col mb-3 border border-neutral-200 dark:border-neutral-800 rounded-md overflow-hidden ${getContainerHeight} relative`}
|
||||
@@ -106,9 +110,9 @@ const SingleWSMessage = ({
|
||||
) : (
|
||||
<IconChevronUp size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
)}
|
||||
<span className="font-medium text-sm">{`Message ${canClientStream ? index + 1 : ''}`}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<WSRequestBodyMode mode={messageFormat} onModeChange={setMessageFormat} />
|
||||
<ToolHint text="Format JSON with proper indentation and spacing" toolhintId={`prettify-msg-${index}`}>
|
||||
<button
|
||||
onClick={onPrettify}
|
||||
@@ -130,7 +134,6 @@ const SingleWSMessage = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className={`flex ${body.ws.length === 1 || !canClientSendMultipleMessages ? 'h-full' : 'h-80'} relative`}>
|
||||
<CodeEditor
|
||||
@@ -142,7 +145,7 @@ const SingleWSMessage = ({
|
||||
onEdit={onEdit}
|
||||
onRun={handleRun}
|
||||
onSave={onSave}
|
||||
mode="application/ld+json"
|
||||
mode={codemirrorMode[messageFormat] ?? 'text/plain'}
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
34
packages/bruno-app/src/utils/codemirror/lang-detect.js
Normal file
34
packages/bruno-app/src/utils/codemirror/lang-detect.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @param {string} snippet
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isXML(snippet){
|
||||
return /<\/?[a-z][\s\S]*>/i.test(snippet)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} snippet
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isJSON(snippet) {
|
||||
try {
|
||||
JSON.parse(snippet);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} snippet
|
||||
* @returns {string}
|
||||
*/
|
||||
export function autoDetectLang(snippet) {
|
||||
if (isJSON(snippet)) {
|
||||
return 'json';
|
||||
}
|
||||
if(isXML(snippet)){
|
||||
return 'xml'
|
||||
}
|
||||
return 'text'
|
||||
}
|
||||
@@ -1016,14 +1016,13 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
try {
|
||||
JSON.parse(messageContent);
|
||||
} catch (error) {
|
||||
console.error('Error validating ws message JSON:', error);
|
||||
return {
|
||||
body: {
|
||||
mode: 'ws',
|
||||
ws: [
|
||||
{
|
||||
name: messageName,
|
||||
content: '{}'
|
||||
content: messageContent
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user