mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 06:28:33 +00:00
This reverts commit a305b41c93.
This commit is contained in:
@@ -2,19 +2,12 @@ import React, { useMemo, useCallback, useRef } from 'react';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import StatusDot from 'components/StatusDot/index';
|
||||
import ActionIcon from 'ui/ActionIcon';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
import { IconPlus, IconWand } from '@tabler/icons';
|
||||
import { find, get } from 'lodash';
|
||||
import { find } from 'lodash';
|
||||
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import ResponsiveTabs from 'ui/ResponsiveTabs';
|
||||
import { getPropertyFromDraftOrRequest } from 'utils/collections/index';
|
||||
import { prettifyJsonString, uuid } from 'utils/common/index';
|
||||
import xmlFormat from 'xml-formatter';
|
||||
import toast from 'react-hot-toast';
|
||||
import WsBody from '../WsBody/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import WSAuth from './WSAuth';
|
||||
@@ -31,8 +24,6 @@ const WSRequestPane = ({ item, collection, handleRun }) => {
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const requestPaneTab = focusedTab?.requestPaneTab;
|
||||
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
|
||||
const selectTab = useCallback(
|
||||
(tab) => {
|
||||
dispatch(updateRequestPaneTab({
|
||||
@@ -43,63 +34,6 @@ const WSRequestPane = ({ item, collection, handleRun }) => {
|
||||
[dispatch, item.uid]
|
||||
);
|
||||
|
||||
const addNewMessage = useCallback(() => {
|
||||
const currentMessages = Array.isArray(body?.ws)
|
||||
? body.ws.map((msg) => ({ ...msg, selected: false }))
|
||||
: [];
|
||||
currentMessages.push({
|
||||
uid: uuid(),
|
||||
name: `message ${currentMessages.length + 1}`,
|
||||
content: '{}',
|
||||
type: 'json',
|
||||
selected: true
|
||||
});
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
}, [body, dispatch, item.uid, collection.uid]);
|
||||
|
||||
const onPrettifyAll = useCallback(() => {
|
||||
const currentMessages = [...(body?.ws || [])];
|
||||
let changed = false;
|
||||
|
||||
currentMessages.forEach((msg, i) => {
|
||||
if (msg.type === 'json') {
|
||||
try {
|
||||
const pretty = prettifyJsonString(msg.content);
|
||||
if (pretty !== msg.content) {
|
||||
currentMessages[i] = { ...msg, content: pretty };
|
||||
changed = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// skip invalid json
|
||||
}
|
||||
} else if (msg.type === 'xml') {
|
||||
try {
|
||||
const pretty = xmlFormat(msg.content, { collapseContent: true });
|
||||
if (pretty !== msg.content) {
|
||||
currentMessages[i] = { ...msg, content: pretty };
|
||||
changed = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// skip invalid xml
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
} else {
|
||||
toast.error('Nothing to prettify');
|
||||
}
|
||||
}, [body, dispatch, item.uid, collection.uid]);
|
||||
|
||||
const headers = getPropertyFromDraftOrRequest(item, 'request.headers');
|
||||
const docs = getPropertyFromDraftOrRequest(item, 'request.docs');
|
||||
const auth = getPropertyFromDraftOrRequest(item, 'request.auth');
|
||||
@@ -143,8 +77,9 @@ const WSRequestPane = ({ item, collection, handleRun }) => {
|
||||
<WsBody
|
||||
item={item}
|
||||
collection={collection}
|
||||
hideModeSelector={true}
|
||||
hidePrettifyButton={true}
|
||||
handleRun={handleRun}
|
||||
onAddMessage={addNewMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -164,41 +99,17 @@ const WSRequestPane = ({ item, collection, handleRun }) => {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
}, [requestPaneTab, item, collection, handleRun, addNewMessage]);
|
||||
}, [requestPaneTab, item, collection, handleRun]);
|
||||
|
||||
if (!activeTabUid || !focusedTab?.uid || !requestPaneTab) {
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
let rightContent = null;
|
||||
if (requestPaneTab === 'auth') {
|
||||
rightContent = (
|
||||
<div ref={rightContentRef} className="flex flex-grow justify-start items-center">
|
||||
<WSAuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
);
|
||||
} else if (requestPaneTab === 'body') {
|
||||
rightContent = (
|
||||
<div ref={rightContentRef} className="flex items-center gap-2">
|
||||
<ToolHint text="Prettify All" toolhintId="prettify-all-ws">
|
||||
<ActionIcon
|
||||
data-testid="ws-prettify-all"
|
||||
onClick={onPrettifyAll}
|
||||
>
|
||||
<IconWand size={14} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
</ToolHint>
|
||||
<ToolHint text="Add Message" toolhintId="add-msg-ws">
|
||||
<ActionIcon
|
||||
data-testid="ws-add-message"
|
||||
onClick={addNewMessage}
|
||||
>
|
||||
<IconPlus size={15} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
</ToolHint>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const rightContent = requestPaneTab === 'auth' ? (
|
||||
<div ref={rightContentRef} className="flex flex-grow justify-start items-center">
|
||||
<WSAuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
|
||||
@@ -1,88 +1,72 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
border-bottom: 1px solid ${(props) => props.theme.border.border0};
|
||||
transition: opacity 0.15s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.45;
|
||||
&.single {
|
||||
height: 100%;
|
||||
|
||||
.editor-container {
|
||||
height: calc(100% - 32px);
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
&:not(.single) {
|
||||
min-height: 240px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.message-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
padding: 4px 0px;
|
||||
padding-top: 0px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.accordion-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
.message-label {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.name-input {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: inherit;
|
||||
background: ${(props) => props.theme.background.surface1};
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
outline: none;
|
||||
flex: 1;
|
||||
}
|
||||
.message-label {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.accordion-actions {
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
|
||||
.hover-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
.hover-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 0.25rem;
|
||||
color: ${(props) => props.theme.text};
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&.delete:hover {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
}
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&:hover .hover-actions {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
.toolbar-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
&.delete:hover {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.disabled) .accordion-header .message-label {
|
||||
color: ${(props) => props.theme.primary.text};
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,117 +1,56 @@
|
||||
import { IconTrash, IconSend, IconChevronRight, IconChevronDown } from '@tabler/icons';
|
||||
import { IconTrash, IconWand } from '@tabler/icons';
|
||||
import CodeEditor from 'components/CodeEditor/index';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
import { get } from 'lodash';
|
||||
import invert from 'lodash/invert';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import React, { useMemo, useState, useEffect, useRef, useCallback } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { queueWsMessage, isWsConnectionActive, connectWS } from 'utils/network/index';
|
||||
import { findCollectionByUid, findEnvironmentInCollection } from 'utils/collections/index';
|
||||
import toast from 'react-hot-toast';
|
||||
import { autoDetectLang } from 'utils/codemirror/lang-detect';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import { prettifyJsonString } from 'utils/common/index';
|
||||
import xmlFormat from 'xml-formatter';
|
||||
import WSRequestBodyMode from '../BodyMode/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const codemirrorMode = {
|
||||
text: 'application/text',
|
||||
xml: 'application/xml',
|
||||
json: 'application/ld+json'
|
||||
export const TYPE_BY_DECODER = {
|
||||
base64: 'binary',
|
||||
json: 'json',
|
||||
xml: 'xml'
|
||||
};
|
||||
|
||||
// Maps stored type to display mode
|
||||
const typeToMode = (type) => {
|
||||
switch (type) {
|
||||
case 'json': return 'json';
|
||||
case 'xml': return 'xml';
|
||||
default: return 'text';
|
||||
}
|
||||
};
|
||||
export const DECODER_BY_TYPE = invert(TYPE_BY_DECODER);
|
||||
|
||||
export const SingleWSMessage = ({
|
||||
message,
|
||||
item,
|
||||
collection,
|
||||
index,
|
||||
methodType,
|
||||
handleRun,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
isNew,
|
||||
onNewRendered,
|
||||
isSelected,
|
||||
onSelect
|
||||
canClientSendMultipleMessages,
|
||||
isLast
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
|
||||
const { name, content, type } = message;
|
||||
const displayMode = typeToMode(type);
|
||||
const displayName = name || `message ${index + 1}`;
|
||||
const [messageFormat, setMessageFormat] = useState(autoDetectLang(content));
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editValue, setEditValue] = useState(displayName);
|
||||
const onUpdateMessageType = (type) => {
|
||||
setMessageFormat(type);
|
||||
|
||||
// Auto-focus the name input when this is a newly created message
|
||||
useEffect(() => {
|
||||
if (isNew) {
|
||||
setIsEditing(true);
|
||||
setEditValue(displayName);
|
||||
onNewRendered();
|
||||
}
|
||||
}, [isNew]);
|
||||
|
||||
const saveName = (value) => {
|
||||
const trimmed = value.trim() || `message ${index + 1}`;
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
|
||||
currentMessages[index] = {
|
||||
...currentMessages[index],
|
||||
name: trimmed
|
||||
type: DECODER_BY_TYPE[type]
|
||||
};
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleNameKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
saveName(editValue);
|
||||
} else if (e.key === 'Escape') {
|
||||
setEditValue(displayName);
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNameBlur = () => {
|
||||
saveName(editValue);
|
||||
};
|
||||
|
||||
const handleNameClick = useCallback((e) => {
|
||||
e.stopPropagation();
|
||||
setEditValue(displayName);
|
||||
setIsEditing(true);
|
||||
}, [displayName, onToggle]);
|
||||
|
||||
const fontSize = get(preferences, 'font.codeFontSize', 14);
|
||||
const lineHeight = fontSize * 1.5;
|
||||
|
||||
const editorHeight = useMemo(() => {
|
||||
const lineCount = (content || '').split('\n').length;
|
||||
const lines = lineCount + 1;
|
||||
return `${lines * lineHeight + 10}px`;
|
||||
}, [content, lineHeight]);
|
||||
|
||||
const onUpdateMessageType = (newMode) => {
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
currentMessages[index] = {
|
||||
...currentMessages[index],
|
||||
type: typeToMode(newMode)
|
||||
};
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
@@ -121,11 +60,13 @@ export const SingleWSMessage = ({
|
||||
|
||||
const onEdit = (value) => {
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
|
||||
currentMessages[index] = {
|
||||
...currentMessages[index],
|
||||
name: name || `message ${index + 1}`,
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
type: DECODER_BY_TYPE[messageFormat],
|
||||
content: value
|
||||
};
|
||||
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
@@ -137,7 +78,9 @@ export const SingleWSMessage = ({
|
||||
|
||||
const onDeleteMessage = () => {
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
|
||||
currentMessages.splice(index, 1);
|
||||
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
@@ -145,112 +88,97 @@ export const SingleWSMessage = ({
|
||||
}));
|
||||
};
|
||||
|
||||
const onSendMessage = useCallback(async () => {
|
||||
try {
|
||||
const col = findCollectionByUid(collections, collection.uid);
|
||||
const environment = findEnvironmentInCollection(col, col?.activeEnvironmentUid);
|
||||
let codeType = messageFormat;
|
||||
if (TYPE_BY_DECODER[type]) {
|
||||
codeType = TYPE_BY_DECODER[type];
|
||||
}
|
||||
|
||||
// Auto-connect if not already connected
|
||||
const connectionStatus = await isWsConnectionActive(item.uid);
|
||||
if (!connectionStatus.isActive) {
|
||||
await connectWS(item, col, environment, col?.runtimeVariables, { connectOnly: true });
|
||||
}
|
||||
const codemirrorMode = {
|
||||
text: 'application/text',
|
||||
xml: 'application/xml',
|
||||
json: 'application/ld+json'
|
||||
};
|
||||
|
||||
const result = await queueWsMessage(item, col, environment, col?.runtimeVariables, index);
|
||||
if (!result.success) {
|
||||
toast.error(result.error || 'Failed to send message');
|
||||
const onPrettify = () => {
|
||||
if (codeType === 'json') {
|
||||
try {
|
||||
const prettyBodyJson = prettifyJsonString(content);
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
currentMessages[index] = {
|
||||
...currentMessages[index],
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
content: prettyBodyJson
|
||||
};
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
} catch (e) {
|
||||
toastError(new Error('Unable to prettify. Invalid JSON format.'));
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to send message');
|
||||
}
|
||||
}, [collections]);
|
||||
|
||||
if (codeType === 'xml') {
|
||||
try {
|
||||
const prettyBodyXML = xmlFormat(content, { collapseContent: true });
|
||||
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
currentMessages[index] = {
|
||||
...currentMessages[index],
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
content: prettyBodyXML
|
||||
};
|
||||
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
} catch (e) {
|
||||
toastError(new Error('Unable to prettify. Invalid XML format.'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isSingleMessage = !canClientSendMultipleMessages || body.ws.length === 1;
|
||||
|
||||
return (
|
||||
<StyledWrapper
|
||||
className={!isSelected ? 'disabled' : ''}
|
||||
onMouseDownCapture={() => {
|
||||
if (!isSelected) setTimeout(onSelect, 0);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="accordion-header"
|
||||
data-testid={`ws-message-header-${index}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onToggle}
|
||||
onKeyDown={(e) => {
|
||||
if (e.target !== e.currentTarget) return;
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onToggle();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="accordion-left">
|
||||
{isExpanded ? (
|
||||
<IconChevronDown size={14} strokeWidth={2} />
|
||||
) : (
|
||||
<IconChevronRight size={14} strokeWidth={2} />
|
||||
)}
|
||||
{isEditing ? (
|
||||
<input
|
||||
ref={(node) => node?.focus()}
|
||||
className="name-input"
|
||||
data-testid={`ws-message-name-input-${index}`}
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={handleNameKeyDown}
|
||||
onBlur={handleNameBlur}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="message-label"
|
||||
data-testid={`ws-message-label-${index}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onToggle();
|
||||
}}
|
||||
onDoubleClick={handleNameClick}
|
||||
>
|
||||
{displayName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="accordion-actions" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="hover-actions">
|
||||
<ToolHint text="Send" toolhintId={`send-msg-${index}`}>
|
||||
<button onClick={onSendMessage} className="hover-action-btn" data-testid={`ws-send-msg-${index}`}>
|
||||
<IconSend size={14} strokeWidth={1.5} />
|
||||
<StyledWrapper className={`message-container ${isSingleMessage ? 'single' : ''} ${isLast ? 'last' : ''}`}>
|
||||
<div className="message-toolbar">
|
||||
<span className="message-label">Message {index + 1}</span>
|
||||
<div className="toolbar-actions">
|
||||
<WSRequestBodyMode mode={messageFormat} onModeChange={onUpdateMessageType} />
|
||||
|
||||
<ToolHint text="Format" toolhintId={`prettify-msg-${index}`}>
|
||||
<button onClick={onPrettify} className="toolbar-btn">
|
||||
<IconWand size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
{index > 0 && (
|
||||
<ToolHint text="Delete message" toolhintId={`delete-msg-${index}`}>
|
||||
<button onClick={onDeleteMessage} className="toolbar-btn delete">
|
||||
<IconTrash size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</ToolHint>
|
||||
{(body.ws || []).length > 1 && (
|
||||
<ToolHint text="Delete" toolhintId={`delete-msg-${index}`}>
|
||||
<button onClick={onDeleteMessage} className="hover-action-btn delete" data-testid={`ws-delete-msg-${index}`}>
|
||||
<IconTrash size={14} strokeWidth={1.5} />
|
||||
</button>
|
||||
</ToolHint>
|
||||
)}
|
||||
</div>
|
||||
<WSRequestBodyMode mode={displayMode} onModeChange={onUpdateMessageType} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div className="accordion-body" data-testid={`ws-message-body-${index}`} style={{ height: editorHeight }}>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
value={content}
|
||||
onEdit={onEdit}
|
||||
onRun={handleRun}
|
||||
onSave={onSave}
|
||||
mode={codemirrorMode[displayMode] ?? 'text/plain'}
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="editor-container">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
value={content}
|
||||
onEdit={onEdit}
|
||||
onRun={handleRun}
|
||||
onSave={onSave}
|
||||
mode={codemirrorMode[codeType] ?? 'text/plain'}
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,10 +5,21 @@ const Wrapper = styled.div`
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.single {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.multi {
|
||||
overflow-y: auto;
|
||||
padding-bottom: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@@ -25,20 +36,13 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.add-message-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 0.875rem;
|
||||
color: ${(props) => props.theme.primary.text};
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.add-message-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 8px;
|
||||
background: ${(props) => props.theme.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,124 +1,99 @@
|
||||
import { get } from 'lodash';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
|
||||
import { IconPlus } from '@tabler/icons';
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Button from 'ui/Button';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { SingleWSMessage } from './SingleWSMessage/index';
|
||||
|
||||
const getSelectedIndex = (messages) => {
|
||||
const idx = messages.findIndex((msg) => msg.selected);
|
||||
return idx >= 0 ? idx : 0;
|
||||
};
|
||||
|
||||
const WSBody = ({ item, collection, handleRun, onAddMessage }) => {
|
||||
const WSBody = ({ item, collection, handleRun }) => {
|
||||
const dispatch = useDispatch();
|
||||
const messagesContainerRef = useRef(null);
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
const messages = body?.ws || [];
|
||||
|
||||
const selectedIndex = getSelectedIndex(messages);
|
||||
const methodType = item.draft ? get(item, 'draft.request.methodType') : get(item, 'request.methodType');
|
||||
const canClientSendMultipleMessages = false;
|
||||
|
||||
// Expand the selected message by default (falls back to first)
|
||||
const [expandedUids, setExpandedUids] = useState(() => {
|
||||
const uid = messages[selectedIndex]?.uid || messages[0]?.uid;
|
||||
return new Set(uid ? [uid] : []);
|
||||
});
|
||||
const [newMessageUid, setNewMessageUid] = useState(null);
|
||||
const prevMessagesLengthRef = useRef(messages.length);
|
||||
|
||||
const setSelectedIndex = useCallback((index) => {
|
||||
const currentMessages = [...(body?.ws || [])];
|
||||
const updated = currentMessages.map((msg, i) => ({
|
||||
...msg,
|
||||
selected: i === index
|
||||
}));
|
||||
dispatch(updateRequestBody({
|
||||
content: updated,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
}, [body, dispatch, item.uid, collection.uid]);
|
||||
|
||||
const toggleMessage = useCallback((uid) => {
|
||||
if (!uid) return;
|
||||
setExpandedUids((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(uid)) {
|
||||
next.delete(uid);
|
||||
} else {
|
||||
next.add(uid);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSelect = useCallback((index) => {
|
||||
if (index !== selectedIndex) {
|
||||
setSelectedIndex(index);
|
||||
}
|
||||
}, [selectedIndex, setSelectedIndex]);
|
||||
|
||||
// React to new message being added (messages.length increased)
|
||||
// Auto-scroll to the latest message when messages are added
|
||||
useEffect(() => {
|
||||
if (messages.length > prevMessagesLengthRef.current) {
|
||||
const newMsg = messages[messages.length - 1];
|
||||
if (newMsg?.uid) {
|
||||
setExpandedUids((prev) => new Set(prev).add(newMsg.uid));
|
||||
setNewMessageUid(newMsg.uid);
|
||||
setSelectedIndex(messages.length - 1);
|
||||
}
|
||||
}
|
||||
prevMessagesLengthRef.current = messages.length;
|
||||
}, [messages.length]);
|
||||
|
||||
const handleNewMessageRendered = useCallback(() => {
|
||||
setNewMessageUid(null);
|
||||
}, []);
|
||||
|
||||
// Auto-scroll to bottom when new message is added
|
||||
useEffect(() => {
|
||||
if (messagesContainerRef.current && messages.length > 0) {
|
||||
if (messagesContainerRef.current && body?.ws?.length > 0) {
|
||||
const container = messagesContainerRef.current;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}, [messages.length]);
|
||||
}, [body?.ws?.length]);
|
||||
|
||||
if (!messages.length) {
|
||||
const addNewMessage = () => {
|
||||
const currentMessages = Array.isArray(body.ws) ? [...body.ws] : [];
|
||||
|
||||
currentMessages.push({
|
||||
name: `message ${currentMessages.length + 1}`,
|
||||
content: '{}'
|
||||
});
|
||||
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
};
|
||||
|
||||
if (!body?.ws || !Array.isArray(body.ws)) {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="empty-state">
|
||||
<p>No WebSocket messages available</p>
|
||||
<button className="add-message-link" data-testid="ws-add-message" onClick={onAddMessage}>
|
||||
<IconPlus size={14} strokeWidth={1.5} />
|
||||
<span>Add message</span>
|
||||
</button>
|
||||
<Button
|
||||
onClick={addNewMessage}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon={<IconPlus size={14} strokeWidth={1.5} />}
|
||||
>
|
||||
Add Message
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const messagesToShow = body.ws.filter((_, index) => canClientSendMultipleMessages || index === 0);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div ref={messagesContainerRef} className="messages-container">
|
||||
{messages.map((message, index) => (
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
className={`messages-container ${canClientSendMultipleMessages && messagesToShow.length > 1 ? 'multi' : 'single'}`}
|
||||
>
|
||||
{messagesToShow.map((message, index) => (
|
||||
<SingleWSMessage
|
||||
key={message.uid}
|
||||
id={`ws-message-${message.uid}`}
|
||||
key={index}
|
||||
message={message}
|
||||
item={item}
|
||||
collection={collection}
|
||||
index={index}
|
||||
methodType={methodType}
|
||||
handleRun={handleRun}
|
||||
isExpanded={expandedUids.has(message.uid)}
|
||||
onToggle={() => toggleMessage(message.uid)}
|
||||
isNew={newMessageUid === message.uid}
|
||||
onNewRendered={handleNewMessageRendered}
|
||||
isSelected={selectedIndex === index}
|
||||
onSelect={() => handleSelect(index)}
|
||||
canClientSendMultipleMessages={canClientSendMultipleMessages}
|
||||
isLast={index === messagesToShow.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{canClientSendMultipleMessages && (
|
||||
<div className="add-message-footer">
|
||||
<Button
|
||||
onClick={addNewMessage}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
fullWidth
|
||||
icon={<IconPlus size={14} strokeWidth={1.5} />}
|
||||
>
|
||||
Add Message
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user