add: draft for collection and folder settings (#5947)

This commit is contained in:
Pooja
2025-11-12 11:11:12 +05:30
committed by GitHub
parent e844d35b03
commit f439f2de9a
65 changed files with 2049 additions and 504 deletions

View File

@@ -6,7 +6,7 @@ import Dropdown from 'components/Dropdown';
import { useTheme } from 'providers/Theme';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { humanizeRequestAPIKeyPlacement } from 'utils/collections';
@@ -16,9 +16,9 @@ const ApiKeyAuth = ({ collection }) => {
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const apikeyAuth = get(collection, 'root.request.auth.apikey', {});
const apikeyAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.apikey', {}) : get(collection, 'root.request.auth.apikey', {});
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const Icon = forwardRef((props, ref) => {
return (

View File

@@ -11,7 +11,7 @@ const AuthMode = ({ collection }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const authMode = get(collection, 'root.request.auth.mode');
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
const Icon = forwardRef((props, ref) => {
return (

View File

@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const AwsV4Auth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const awsv4Auth = get(collection, 'root.request.auth.awsv4', {});
const awsv4Auth = collection.draft?.root ? get(collection, 'draft.root.request.auth.awsv4', {}) : get(collection, 'root.request.auth.awsv4', {});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey);
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleAccessKeyIdChange = (accessKeyId) => {
dispatch(

View File

@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const BasicAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const basicAuth = get(collection, 'root.request.auth.basic', {});
const basicAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.basic', {}) : get(collection, 'root.request.auth.basic', {});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(basicAuth?.password);
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleUsernameChange = (username) => {
dispatch(

View File

@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const BearerAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const bearerToken = get(collection, 'root.request.auth.bearer.token', '');
const bearerToken = collection.draft?.root ? get(collection, 'draft.root.request.auth.bearer.token', '') : get(collection, 'root.request.auth.bearer.token', '');
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(bearerToken);
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleTokenChange = (token) => {
dispatch(

View File

@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const DigestAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const digestAuth = get(collection, 'root.request.auth.digest', {});
const digestAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.digest', {}) : get(collection, 'root.request.auth.digest', {});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(digestAuth?.password);
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleUsernameChange = (username) => {
dispatch(

View File

@@ -6,7 +6,7 @@ import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
@@ -19,11 +19,11 @@ const NTLMAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const ntlmAuth = get(collection, 'root.request.auth.ntlm', {});
const ntlmAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.ntlm', {}) : get(collection, 'root.request.auth.ntlm', {});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password);
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleUsernameChange = (username) => {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import get from 'lodash/get';
import StyledWrapper from './StyledWrapper';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
import { useDispatch } from 'react-redux';
@@ -14,10 +14,10 @@ const GrantTypeComponentMap = ({collection }) => {
const dispatch = useDispatch();
const save = () => {
dispatch(saveCollectionRoot(collection.uid));
dispatch(saveCollectionSettings(collection.uid));
};
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
let request = collection.draft?.root ? get(collection, 'draft.root.request', {}) : get(collection, 'root.request', {});
const grantType = get(request, 'auth.oauth2.grantType', {});
switch (grantType) {
@@ -40,7 +40,7 @@ const GrantTypeComponentMap = ({collection }) => {
};
const OAuth2 = ({ collection }) => {
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
let request = collection.draft?.root ? get(collection, 'draft.root.request', {}) : get(collection, 'root.request', {});
return (
<StyledWrapper className="mt-2 w-full">

View File

@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const WsseAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const wsseAuth = get(collection, 'root.request.auth.wsse', {});
const wsseAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.wsse', {}) : get(collection, 'root.request.auth.wsse', {});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(wsseAuth?.password);
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleUserChange = (username) => {
dispatch(

View File

@@ -8,17 +8,17 @@ import BasicAuth from './BasicAuth';
import DigestAuth from './DigestAuth';
import WsseAuth from './WsseAuth';
import ApiKeyAuth from './ApiKeyAuth/';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import OAuth2 from './OAuth2';
import NTLMAuth from './NTLMAuth';
const Auth = ({ collection }) => {
const authMode = get(collection, 'root.request.auth.mode');
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
const dispatch = useDispatch();
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const getAuthView = () => {
switch (authMode) {

View File

@@ -9,8 +9,18 @@ import SensitiveFieldWarning from 'components/SensitiveFieldWarning/index';
import SingleLineEditor from 'components/SingleLineEditor/index';
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField/index';
import { useTheme } from 'styled-components';
import { useDispatch } from 'react-redux';
import { updateCollectionClientCertificates } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import get from 'lodash/get';
const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }) => {
const ClientCertSettings = ({ collection }) => {
const dispatch = useDispatch();
// Get client certs from draft if exists, otherwise from brunoConfig
const clientCertConfig = collection.draft?.brunoConfig
? get(collection, 'draft.brunoConfig.clientCertificates.certs', [])
: get(collection, 'brunoConfig.clientCertificates.certs', []);
const certFilePathInputRef = useRef();
const keyFilePathInputRef = useRef();
const pfxFilePathInputRef = useRef();
@@ -63,7 +73,19 @@ const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }
passphrase: values.passphrase
};
}
onUpdate(relevantValues);
// Add the new cert to the existing certs in draft
const updatedCerts = [...clientCertConfig, relevantValues];
const clientCertificates = {
enabled: true,
certs: updatedCerts
};
dispatch(updateCollectionClientCertificates({
collectionUid: collection.uid,
clientCertificates
}));
formik.resetForm();
resetFileInputFields();
}
@@ -81,9 +103,15 @@ const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }
};
const resetFileInputFields = () => {
certFilePathInputRef.current.value = '';
keyFilePathInputRef.current.value = '';
pfxFilePathInputRef.current.value = '';
if (certFilePathInputRef.current) {
certFilePathInputRef.current.value = '';
}
if (keyFilePathInputRef.current) {
keyFilePathInputRef.current.value = '';
}
if (pfxFilePathInputRef.current) {
pfxFilePathInputRef.current.value = '';
}
};
const handleTypeChange = (e) => {
@@ -99,6 +127,21 @@ const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }
}
};
const handleRemove = (indexToRemove) => {
const updatedCerts = clientCertConfig.filter((cert, index) => index !== indexToRemove);
const clientCertificates = {
enabled: true,
certs: updatedCerts
};
dispatch(updateCollectionClientCertificates({
collectionUid: collection.uid,
clientCertificates
}));
};
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
return (
<StyledWrapper className="w-full h-full">
<div className="text-xs mb-4 text-muted">Add client certificates to be used for specific domains.</div>
@@ -118,9 +161,9 @@ const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
</div>
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
<button onClick={() => handleRemove(index)} className="remove-certificate ml-2">
<IconTrash size={18} strokeWidth={1.5} />
</button>
</button>
</div>
</li>
))}
@@ -329,10 +372,14 @@ const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }
<div className="ml-1 text-red-500">{formik.errors.passphrase}</div>
) : null}
</div>
<div className="mt-6">
<div className="mt-6 flex flex-row gap-2 items-center">
<button type="submit" className="submit btn btn-sm btn-secondary">
Add
</button>
<div className="h-4 border-l border-gray-600"></div>
<button type="button" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</form>
</StyledWrapper>

View File

@@ -1,10 +1,10 @@
import 'github-markdown-css/github-markdown.css';
import get from 'lodash/get';
import { updateCollectionDocs } from 'providers/ReduxStore/slices/collections';
import { updateCollectionDocs, deleteCollectionDraft } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import Markdown from 'components/MarkDown';
import CodeEditor from 'components/CodeEditor';
import StyledWrapper from './StyledWrapper';
@@ -14,7 +14,7 @@ const Docs = ({ collection }) => {
const dispatch = useDispatch();
const { displayedTheme } = useTheme();
const [isEditing, setIsEditing] = useState(false);
const docs = get(collection, 'root.docs', '');
const docs = collection.draft?.root ? get(collection, 'draft.root.docs', '') : get(collection, 'root.docs', '');
const preferences = useSelector((state) => state.app.preferences);
const toggleViewMode = () => {
@@ -31,17 +31,17 @@ const Docs = ({ collection }) => {
};
const handleDiscardChanges = () => {
dispatch(
dispatch((
updateCollectionDocs({
collectionUid: collection.uid,
docs: docs
})
}))
);
toggleViewMode();
}
const onSave = () => {
dispatch(saveCollectionRoot(collection.uid));
dispatch(saveCollectionSettings(collection.uid));
toggleViewMode();
}

View File

@@ -10,7 +10,7 @@ import {
deleteCollectionHeader,
setCollectionHeaders
} from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
@@ -21,7 +21,7 @@ const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const Headers = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const headers = get(collection, 'root.request.headers', []);
const headers = collection.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
const toggleBulkEditMode = () => {
@@ -40,7 +40,7 @@ const Headers = ({ collection }) => {
);
};
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleHeaderValueChange = (e, _header, type) => {
const header = cloneDeep(_header);
switch (type) {

View File

@@ -1,37 +1,44 @@
import React from 'react';
import { useFormik } from 'formik';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
import cloneDeep from 'lodash/cloneDeep';
import { updateCollectionPresets } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { get } from 'lodash';
const PresetsSettings = ({ collection }) => {
const dispatch = useDispatch();
const {
brunoConfig: { presets: presets = {} }
} = collection;
const initialPresets = { requestType: 'http', requestUrl: '' };
const formik = useFormik({
enableReinitialize: true,
initialValues: {
requestType: presets.requestType || 'http',
requestUrl: presets.requestUrl || ''
},
onSubmit: (newPresets) => {
const brunoConfig = cloneDeep(collection.brunoConfig);
brunoConfig.presets = newPresets;
dispatch(updateBrunoConfig(brunoConfig, collection.uid));
toast.success('Collection presets updated');
}
});
// Get presets from draft.brunoConfig if it exists, otherwise from brunoConfig
const currentPresets = collection.draft?.brunoConfig
? get(collection, 'draft.brunoConfig.presets', initialPresets)
: get(collection, 'brunoConfig.presets', initialPresets);
// Helper to update presets config
const updatePresets = (updates) => {
const updatedPresets = { ...currentPresets, ...updates };
dispatch(updateCollectionPresets({
collectionUid: collection.uid,
presets: updatedPresets
}));
};
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleRequestTypeChange = (e) => {
updatePresets({ requestType: e.target.value });
};
const handleRequestUrlChange = (e) => {
updatePresets({ requestUrl: e.target.value });
};
return (
<StyledWrapper className="h-full w-full">
<div className="text-xs mb-4 text-muted">
These presets will be used as the default values for new requests in this collection.
</div>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="bruno-form">
<div className="mb-3 flex items-center">
<label className="settings-label flex items-center" htmlFor="enabled">
Request Type
@@ -42,9 +49,9 @@ const PresetsSettings = ({ collection }) => {
className="cursor-pointer"
type="radio"
name="requestType"
onChange={formik.handleChange}
onChange={handleRequestTypeChange}
value="http"
checked={formik.values.requestType === 'http'}
checked={(currentPresets.requestType || 'http') === 'http'}
/>
<label htmlFor="http" className="ml-1 cursor-pointer select-none">
HTTP
@@ -55,9 +62,9 @@ const PresetsSettings = ({ collection }) => {
className="ml-4 cursor-pointer"
type="radio"
name="requestType"
onChange={formik.handleChange}
onChange={handleRequestTypeChange}
value="graphql"
checked={formik.values.requestType === 'graphql'}
checked={(currentPresets.requestType || 'http') === 'graphql'}
/>
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
GraphQL
@@ -68,9 +75,9 @@ const PresetsSettings = ({ collection }) => {
className="ml-4 cursor-pointer"
type="radio"
name="requestType"
onChange={formik.handleChange}
onChange={handleRequestTypeChange}
value="grpc"
checked={formik.values.requestType === 'grpc'}
checked={(currentPresets.requestType || 'http') === 'grpc'}
/>
<label htmlFor="grpc" className="ml-1 cursor-pointer select-none">
gRPC
@@ -93,8 +100,8 @@ const PresetsSettings = ({ collection }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.requestUrl || ''}
onChange={handleRequestUrlChange}
value={currentPresets.requestUrl || ''}
style={{ width: '100%' }}
/>
</div>
@@ -102,11 +109,11 @@ const PresetsSettings = ({ collection }) => {
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</form>
</div>
</StyledWrapper>
);
};

View File

@@ -1,4 +1,5 @@
import React, { useRef } from 'react';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import {
IconTrash,
@@ -10,8 +11,10 @@ import {
import { getBasename } from 'utils/common/path';
import { Tooltip } from 'react-tooltip';
import useProtoFileManagement from '../../../hooks/useProtoFileManagement';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
const ProtobufSettings = ({ collection }) => {
const dispatch = useDispatch();
const {
protoFiles,
importPaths,
@@ -27,6 +30,8 @@ const ProtobufSettings = ({ collection }) => {
} = useProtoFileManagement(collection);
const fileInputRef = useRef(null);
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
// Get file path using the ipcRenderer
const getProtoFile = async (event) => {
const files = event?.files;
@@ -164,7 +169,7 @@ const ProtobufSettings = ({ collection }) => {
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
<div className="flex items-center">
<IconFile size={16} className="text-gray-500 dark:text-gray-400 mr-2" />
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100" data-testid="protobuf-proto-file-name">
{getBasename(collection.pathname, file.path)}
</span>
{!isValid && <IconAlertCircle size={12} className="text-red-600 dark:text-red-400 ml-2" />}
@@ -329,6 +334,12 @@ const ProtobufSettings = ({ collection }) => {
</div>
</div>
<div className="mt-6">
<button type="button" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</StyledWrapper>
);
};

View File

@@ -1,106 +1,155 @@
import React, { useEffect } from 'react';
import { useFormik } from 'formik';
import React from 'react';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import * as Yup from 'yup';
import toast from 'react-hot-toast';
import { IconEye, IconEyeOff } from '@tabler/icons';
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { updateCollectionProxy } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { get } from 'lodash';
import toast from 'react-hot-toast';
const ProxySettings = ({ proxyConfig, onUpdate }) => {
const proxySchema = Yup.object({
enabled: Yup.string().oneOf(['global', 'true', 'false']),
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string()
.when('enabled', {
is: 'true',
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
otherwise: (hostname) => hostname.nullable()
})
.max(1024),
port: Yup.number()
.min(1)
.max(65535)
.typeError('Specify port between 1 and 65535')
.nullable()
.transform((_, val) => (val ? Number(val) : null)),
auth: Yup.object()
.when('enabled', {
is: 'true',
then: Yup.object({
enabled: Yup.boolean(),
username: Yup.string()
.when('enabled', {
is: true,
then: (username) => username.required('Specify username for proxy authentication.')
})
.max(1024),
password: Yup.string()
.when('enabled', {
is: true,
then: (password) => password.required('Specify password for proxy authentication.')
})
.max(1024)
})
})
.optional(),
bypassProxy: Yup.string().optional().max(1024)
});
const ProxySettings = ({ collection }) => {
const dispatch = useDispatch();
const initialProxyConfig = { enabled: 'global', protocol: 'http', hostname: '', port: '', auth: { enabled: false, username: '', password: '' }, bypassProxy: '' };
const formik = useFormik({
initialValues: {
enabled: proxyConfig.enabled || 'global',
protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '',
port: proxyConfig.port || '',
auth: {
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
},
bypassProxy: proxyConfig.bypassProxy || ''
},
validationSchema: proxySchema,
onSubmit: (values) => {
proxySchema
.validate(values, { abortEarly: true })
.then((validatedProxy) => {
// serialize 'enabled' to boolean
if (validatedProxy.enabled === 'true') {
validatedProxy.enabled = true;
} else if (validatedProxy.enabled === 'false') {
validatedProxy.enabled = false;
}
// Get proxy from draft.brunoConfig if it exists, otherwise from brunoConfig
const currentProxyConfig = collection.draft?.brunoConfig
? get(collection, 'draft.brunoConfig.proxy', initialProxyConfig)
: get(collection, 'brunoConfig.proxy', initialProxyConfig);
onUpdate(validatedProxy);
})
.catch((error) => {
let errMsg = error.message || 'Preferences validation error';
toast.error(errMsg);
});
}
});
const [passwordVisible, setPasswordVisible] = useState(false);
useEffect(() => {
formik.setValues({
enabled: proxyConfig.enabled === true ? 'true' : proxyConfig.enabled === false ? 'false' : 'global',
protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '',
port: proxyConfig.port || '',
const validateHostnameOnChange = (hostname) => {
if (hostname && hostname.length > 1024) {
toast.error('Hostname must be less than 1024 characters');
return false;
}
return true;
};
const validatePortOnChange = (port) => {
if (!port || port === '') {
return true; // Allow empty port during typing
}
const portNum = Number(port);
if (isNaN(portNum)) {
toast.error('Port must be a valid number');
return false;
}
if (portNum < 1 || portNum > 65535) {
toast.error('Port must be between 1 and 65535');
return false;
}
return true;
};
const validateAuthUsernameOnChange = (username) => {
if (username && username.length > 1024) {
toast.error('Username must be less than 1024 characters');
return false;
}
return true;
};
const validateAuthPasswordOnChange = (password) => {
if (password && password.length > 1024) {
toast.error('Password must be less than 1024 characters');
return false;
}
return true;
};
const validateBypassProxyOnChange = (bypassProxy) => {
if (bypassProxy && bypassProxy.length > 1024) {
toast.error('Bypass proxy must be less than 1024 characters');
return false;
}
return true;
};
// Helper to update proxy config
const updateProxy = (updates) => {
const updatedProxy = { ...currentProxyConfig, ...updates };
dispatch(updateCollectionProxy({
collectionUid: collection.uid,
proxy: updatedProxy
}));
};
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleEnabledChange = (e) => {
const value = e.target.value;
// Convert string to boolean or keep as 'global'
const enabled = value === 'true' ? true : value === 'false' ? false : 'global';
updateProxy({ enabled });
};
const handleProtocolChange = (e) => {
updateProxy({ protocol: e.target.value });
};
const handleHostnameChange = (e) => {
const hostname = e.target.value;
if (validateHostnameOnChange(hostname)) {
updateProxy({ hostname });
}
};
const handlePortChange = (e) => {
const port = e.target.value ? Number(e.target.value) : '';
if (validatePortOnChange(port)) {
updateProxy({ port });
}
};
const handleAuthEnabledChange = (e) => {
updateProxy({
auth: {
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
},
bypassProxy: proxyConfig.bypassProxy || ''
...currentProxyConfig.auth,
enabled: e.target.checked
}
});
}, [proxyConfig]);
};
const handleAuthUsernameChange = (e) => {
const username = e.target.value;
if (validateAuthUsernameOnChange(username)) {
updateProxy({
auth: {
...currentProxyConfig.auth,
username
}
});
}
};
const handleAuthPasswordChange = (e) => {
const password = e.target.value;
if (validateAuthPasswordOnChange(password)) {
updateProxy({
auth: {
...currentProxyConfig.auth,
password
}
});
}
};
const handleBypassProxyChange = (e) => {
const bypassProxy = e.target.value;
if (validateBypassProxyOnChange(bypassProxy)) {
updateProxy({ bypassProxy });
}
};
const enabledValue = currentProxyConfig.enabled === true ? 'true' : currentProxyConfig.enabled === false ? 'false' : 'global';
return (
<StyledWrapper className="h-full w-full">
<div className="text-xs mb-4 text-muted">Configure proxy settings for this collection.</div>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="bruno-form">
<div className="mb-3 flex items-center">
<label className="settings-label flex items-center" htmlFor="enabled">
Config
@@ -120,8 +169,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
type="radio"
name="enabled"
value="global"
checked={formik.values.enabled === 'global'}
onChange={formik.handleChange}
checked={enabledValue === 'global'}
onChange={handleEnabledChange}
className="mr-1"
/>
global
@@ -130,9 +179,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<input
type="radio"
name="enabled"
value={'true'}
checked={formik.values.enabled === 'true'}
onChange={formik.handleChange}
value="true"
checked={enabledValue === 'true'}
onChange={handleEnabledChange}
className="mr-1"
/>
enabled
@@ -141,9 +190,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<input
type="radio"
name="enabled"
value={'false'}
checked={formik.values.enabled === 'false'}
onChange={formik.handleChange}
value="false"
checked={enabledValue === 'false'}
onChange={handleEnabledChange}
className="mr-1"
/>
disabled
@@ -160,8 +209,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
type="radio"
name="protocol"
value="http"
checked={formik.values.protocol === 'http'}
onChange={formik.handleChange}
checked={(currentProxyConfig.protocol || 'http') === 'http'}
onChange={handleProtocolChange}
className="mr-1"
/>
HTTP
@@ -171,8 +220,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
type="radio"
name="protocol"
value="https"
checked={formik.values.protocol === 'https'}
onChange={formik.handleChange}
checked={(currentProxyConfig.protocol || 'http') === 'https'}
onChange={handleProtocolChange}
className="mr-1"
/>
HTTPS
@@ -182,8 +231,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
type="radio"
name="protocol"
value="socks4"
checked={formik.values.protocol === 'socks4'}
onChange={formik.handleChange}
checked={(currentProxyConfig.protocol || 'http') === 'socks4'}
onChange={handleProtocolChange}
className="mr-1"
/>
SOCKS4
@@ -193,8 +242,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
type="radio"
name="protocol"
value="socks5"
checked={formik.values.protocol === 'socks5'}
onChange={formik.handleChange}
checked={(currentProxyConfig.protocol || 'http') === 'socks5'}
onChange={handleProtocolChange}
className="mr-1"
/>
SOCKS5
@@ -214,12 +263,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.hostname || ''}
onChange={handleHostnameChange}
value={currentProxyConfig.hostname || ''}
/>
{formik.touched.hostname && formik.errors.hostname ? (
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="port">
@@ -234,12 +280,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.port}
onChange={handlePortChange}
value={currentProxyConfig.port || ''}
/>
{formik.touched.port && formik.errors.port ? (
<div className="ml-3 text-red-500">{formik.errors.port}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.enabled">
@@ -248,8 +291,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<input
type="checkbox"
name="auth.enabled"
checked={formik.values.auth.enabled}
onChange={formik.handleChange}
checked={currentProxyConfig.auth?.enabled || false}
onChange={handleAuthEnabledChange}
/>
</div>
<div>
@@ -266,12 +309,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.username}
onChange={formik.handleChange}
value={currentProxyConfig.auth?.username || ''}
onChange={handleAuthUsernameChange}
/>
{formik.touched.auth?.username && formik.errors.auth?.username ? (
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.password">
@@ -287,8 +327,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.password}
onChange={formik.handleChange}
value={currentProxyConfig.auth?.password || ''}
onChange={handleAuthPasswordChange}
/>
<button
type="button"
@@ -298,9 +338,6 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
</button>
</div>
{formik.touched.auth?.password && formik.errors.auth?.password ? (
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
) : null}
</div>
</div>
<div className="mb-3 flex items-center">
@@ -316,19 +353,16 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.bypassProxy || ''}
onChange={handleBypassProxyChange}
value={currentProxyConfig.bypassProxy || ''}
/>
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
) : null}
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</form>
</div>
</StyledWrapper>
);
};

View File

@@ -3,14 +3,14 @@ import get from 'lodash/get';
import { useDispatch, useSelector } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateCollectionRequestScript, updateCollectionResponseScript } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
const Script = ({ collection }) => {
const dispatch = useDispatch();
const requestScript = get(collection, 'root.request.script.req', '');
const responseScript = get(collection, 'root.request.script.res', '');
const requestScript = collection.draft?.root ? get(collection, 'draft.root.request.script.req', '') : get(collection, 'root.request.script.req', '');
const responseScript = collection.draft?.root ? get(collection, 'draft.root.request.script.res', '') : get(collection, 'root.request.script.res', '');
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
@@ -34,7 +34,7 @@ const Script = ({ collection }) => {
};
const handleSave = () => {
dispatch(saveCollectionRoot(collection.uid));
dispatch(saveCollectionSettings(collection.uid));
};
return (

View File

@@ -3,13 +3,13 @@ import get from 'lodash/get';
import { useDispatch, useSelector } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateCollectionTests } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
const Tests = ({ collection }) => {
const dispatch = useDispatch();
const tests = get(collection, 'root.request.tests', '');
const tests = collection.draft?.root ? get(collection, 'draft.root.request.tests', '') : get(collection, 'root.request.tests', '');
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
@@ -23,7 +23,7 @@ const Tests = ({ collection }) => {
);
};
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
return (
<StyledWrapper className="w-full flex flex-col h-full">

View File

@@ -3,7 +3,7 @@ import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
@@ -28,7 +28,7 @@ const VarsTable = ({ collection, vars, varType }) => {
);
};
const onSave = () => dispatch(saveCollectionRoot(collection.uid));
const onSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleVarChange = (e, v, type) => {
const _var = cloneDeep(v);
switch (type) {

View File

@@ -2,14 +2,14 @@ import React from 'react';
import get from 'lodash/get';
import VarsTable from './VarsTable';
import StyledWrapper from './StyledWrapper';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
const Vars = ({ collection }) => {
const dispatch = useDispatch();
const requestVars = get(collection, 'root.request.vars.req', []);
const responseVars = get(collection, 'root.request.vars.res', []);
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
const responseVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.res', []) : get(collection, 'root.request.vars.res', []);
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">

View File

@@ -1,9 +1,6 @@
import React from 'react';
import classnames from 'classnames';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import toast from 'react-hot-toast';
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
import { updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
import { useDispatch } from 'react-redux';
import ProxySettings from './ProxySettings';
@@ -31,65 +28,26 @@ const CollectionSettings = ({ collection }) => {
);
};
const root = collection?.root;
const root = collection?.draft?.root || collection?.root;
const hasScripts = root?.request?.script?.res || root?.request?.script?.req;
const hasTests = root?.request?.tests;
const hasDocs = root?.docs;
const headers = get(collection, 'root.request.headers', []);
const headers = collection.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
const activeHeadersCount = headers.filter((header) => header.enabled).length;
const requestVars = get(collection, 'root.request.vars.req', []);
const responseVars = get(collection, 'root.request.vars.res', []);
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
const responseVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.res', []) : get(collection, 'root.request.vars.res', []);
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
const authMode = get(collection, 'root.request.auth', {}).mode || 'none';
const authMode = (collection.draft?.root ? get(collection, 'draft.root.request.auth', {}) : get(collection, 'root.request.auth', {})).mode || 'none';
const presets = get(collection, 'brunoConfig.presets', []);
const presets = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.presets', []) : get(collection, 'brunoConfig.presets', []);
const hasPresets = presets && presets.requestUrl !== '';
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
const proxyConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.proxy', {}) : get(collection, 'brunoConfig.proxy', {});
const proxyEnabled = proxyConfig.hostname ? true : false;
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
const protobufConfig = get(collection, 'brunoConfig.protobuf', {});
const onProxySettingsUpdate = (config) => {
const brunoConfig = cloneDeep(collection.brunoConfig);
brunoConfig.proxy = config;
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
.then(() => {
toast.success('Collection settings updated successfully.');
})
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
};
const onClientCertSettingsUpdate = (config) => {
const brunoConfig = cloneDeep(collection.brunoConfig);
if (!brunoConfig.clientCertificates) {
brunoConfig.clientCertificates = {
enabled: true,
certs: [config]
};
} else {
brunoConfig.clientCertificates.certs.push(config);
}
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
.then(() => {
toast.success('Collection settings updated successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
};
const onClientCertSettingsRemove = (config) => {
const brunoConfig = cloneDeep(collection.brunoConfig);
brunoConfig.clientCertificates.certs = brunoConfig.clientCertificates.certs.filter(
(item) => item.domain != config.domain
);
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
.then(() => {
toast.success('Collection settings updated successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
};
const clientCertConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.clientCertificates.certs', []) : get(collection, 'brunoConfig.clientCertificates.certs', []);
const protobufConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.protobuf', {}) : get(collection, 'brunoConfig.protobuf', {});
const getTabPanel = (tab) => {
switch (tab) {
@@ -115,15 +73,12 @@ const CollectionSettings = ({ collection }) => {
return <Presets collection={collection} />;
}
case 'proxy': {
return <ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />;
return <ProxySettings collection={collection} />;
}
case 'clientCert': {
return (
<ClientCertSettings
collection={collection}
clientCertConfig={clientCertConfig}
onUpdate={onClientCertSettingsUpdate}
onRemove={onClientCertSettingsRemove}
/>
);
}

View File

@@ -26,7 +26,8 @@ const GrantTypeComponentMap = ({ collection, folder }) => {
dispatch(saveFolderRoot(collection.uid, folder.uid));
};
let request = get(folder, 'root.request', {});
const folderRoot = folder?.draft || folder?.root;
let request = get(folderRoot, 'request', {});
const grantType = get(request, 'auth.oauth2.grantType', 'authorization_code');
switch (grantType) {
@@ -45,13 +46,15 @@ const GrantTypeComponentMap = ({ collection, folder }) => {
const Auth = ({ collection, folder }) => {
const dispatch = useDispatch();
let request = get(folder, 'root.request', {});
const authMode = get(folder, 'root.request.auth.mode');
const folderRoot = folder?.draft || folder?.root;
let request = get(folderRoot, 'request', {});
const authMode = get(folderRoot, 'request.auth.mode');
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionAuth = get(collection, 'root.request.auth');
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',
@@ -66,7 +69,8 @@ const Auth = ({ collection, folder }) => {
for (let i = 0; i < folderTreePath.length - 1; i++) {
const parentFolder = folderTreePath[i];
if (parentFolder.type === 'folder') {
const folderAuth = get(parentFolder, 'root.request.auth');
const parentFolderRoot = parentFolder?.draft || parentFolder?.root;
const folderAuth = get(parentFolderRoot, 'request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'inherit') {
effectiveSource = {
type: 'folder',

View File

@@ -11,7 +11,7 @@ const AuthMode = ({ collection, folder }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const authMode = get(folder, 'root.request.auth.mode');
const authMode = folder.draft ? get(folder, 'draft.request.auth.mode') : get(folder, 'root.request.auth.mode');
const Icon = forwardRef((props, ref) => {
return (

View File

@@ -14,7 +14,7 @@ const Documentation = ({ collection, folder }) => {
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const [isEditing, setIsEditing] = useState(false);
const docs = get(folder, 'root.docs', '');
const docs = folder.draft ? get(folder, 'draft.docs', '') : get(folder, 'root.docs', '');
const toggleViewMode = () => {
setIsEditing((prev) => !prev);

View File

@@ -16,7 +16,7 @@ const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const Headers = ({ collection, folder }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const headers = get(folder, 'root.request.headers', []);
const headers = folder.draft ? get(folder, 'draft.request.headers', []) : get(folder, 'root.request.headers', []);
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
const toggleBulkEditMode = () => {

View File

@@ -9,8 +9,8 @@ import StyledWrapper from './StyledWrapper';
const Script = ({ collection, folder }) => {
const dispatch = useDispatch();
const requestScript = get(folder, 'root.request.script.req', '');
const responseScript = get(folder, 'root.request.script.res', '');
const requestScript = folder.draft ? get(folder, 'draft.request.script.req', '') : get(folder, 'root.request.script.req', '');
const responseScript = folder.draft ? get(folder, 'draft.request.script.res', '') : get(folder, 'root.request.script.res', '');
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);

View File

@@ -9,7 +9,7 @@ import StyledWrapper from './StyledWrapper';
const Tests = ({ collection, folder }) => {
const dispatch = useDispatch();
const tests = get(folder, 'root.request.tests', '');
const tests = folder.draft ? get(folder, 'draft.request.tests', '') : get(folder, 'root.request.tests', '');
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);

View File

@@ -7,8 +7,8 @@ import { useDispatch } from 'react-redux';
const Vars = ({ collection, folder }) => {
const dispatch = useDispatch();
const requestVars = get(folder, 'root.request.vars.req', []);
const responseVars = get(folder, 'root.request.vars.res', []);
const requestVars = folder.draft ? get(folder, 'draft.request.vars.req', []) : get(folder, 'root.request.vars.req', []);
const responseVars = folder.draft ? get(folder, 'draft.request.vars.res', []) : get(folder, 'root.request.vars.res', []);
const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
return (
<StyledWrapper className="w-full flex flex-col">

View File

@@ -20,7 +20,7 @@ const FolderSettings = ({ collection, folder }) => {
tab = folderLevelSettingsSelectedTab[folder?.uid];
}
const folderRoot = folder?.root;
const folderRoot = folder?.draft || folder?.root;
const hasScripts = folderRoot?.request?.script?.res || folderRoot?.request?.script?.req;
const hasTests = folderRoot?.request?.tests;

View File

@@ -45,7 +45,8 @@ const Auth = ({ item, collection }) => {
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionAuth = get(collection, 'root.request.auth');
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',

View File

@@ -39,7 +39,7 @@ const ProtoFileDropdown = ({
return;
}
const { success: addSuccess, relativePath, alreadyExists, error: addError } = await protoFileManagement.addProtoFileToCollection(filePath);
const { success: addSuccess, relativePath, alreadyExists, error: addError } = await protoFileManagement.addProtoFileFromRequest(filePath);
if (!addSuccess) {
if (addError) {
toast.error(`Failed to add proto file: ${addError.message}`);
@@ -91,7 +91,7 @@ const ProtoFileDropdown = ({
return;
}
const { success: addSuccess, error: addError } = await protoFileManagement.addImportPathToCollection(directoryPath);
const { success: addSuccess, error: addError } = await protoFileManagement.addImportPathFromRequest(directoryPath);
if (!addSuccess) {
if (addError) {
toast.error(`Failed to add import path: ${addError.message}`);
@@ -103,7 +103,7 @@ const ProtoFileDropdown = ({
};
const handleToggleImportPath = async (index) => {
const { success, enabled, error } = await protoFileManagement.toggleImportPath(index);
const { success, enabled, error } = await protoFileManagement.toggleImportPathFromRequest(index);
if (!success) {
if (error) {
toast.error(`Failed to toggle import path: ${error.message}`);

View File

@@ -48,7 +48,8 @@ const GrpcAuth = ({ item, collection }) => {
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionAuth = get(collection, 'root.request.auth');
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',

View File

@@ -40,7 +40,8 @@ const WSAuth = ({ item, collection }) => {
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionAuth = get(collection, 'root.request.auth');
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { IconAlertTriangle } from '@tabler/icons';
import Modal from 'components/Modal';
const ConfirmCollectionClose = ({ collection, onCancel, onCloseWithoutSave, onSaveAndClose }) => {
return (
<Modal
size="md"
title="Unsaved changes"
confirmText="Save and Close"
cancelText="Close without saving"
disableEscapeKey={true}
disableCloseOnOutsideClick={true}
closeModalFadeTimeout={150}
handleCancel={onCancel}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
hideFooter={true}
>
<div className="flex items-center font-normal">
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
</div>
<div className="font-normal mt-4">
You have unsaved changes in <span className="font-semibold">{collection.name}</span> collection settings.
</div>
<div className="flex justify-between mt-6">
<div>
<button className="btn btn-sm btn-danger" onClick={onCloseWithoutSave}>
Don't Save
</button>
</div>
<div>
<button className="btn btn-close btn-sm mr-2" onClick={onCancel}>
Cancel
</button>
<button className="btn btn-secondary btn-sm" onClick={onSaveAndClose}>
Save
</button>
</div>
</div>
</Modal>
);
};
export default ConfirmCollectionClose;

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { IconAlertTriangle } from '@tabler/icons';
import Modal from 'components/Modal';
const ConfirmFolderClose = ({ folder, onCancel, onCloseWithoutSave, onSaveAndClose }) => {
return (
<Modal
size="md"
title="Unsaved changes"
confirmText="Save and Close"
cancelText="Close without saving"
disableEscapeKey={true}
disableCloseOnOutsideClick={true}
closeModalFadeTimeout={150}
handleCancel={onCancel}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
hideFooter={true}
>
<div className="flex items-center font-normal">
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
</div>
<div className="font-normal mt-4">
You have unsaved changes in <span className="font-semibold">{folder.name}</span> folder settings.
</div>
<div className="flex justify-between mt-6">
<div>
<button className="btn btn-sm btn-danger" onClick={onCloseWithoutSave}>
Don't Save
</button>
</div>
<div>
<button className="btn btn-close btn-sm mr-2" onClick={onCancel}>
Cancel
</button>
<button className="btn btn-secondary btn-sm" onClick={onSaveAndClose}>
Save
</button>
</div>
</div>
</Modal>
);
};
export default ConfirmFolderClose;

View File

@@ -1,8 +1,9 @@
import React from 'react';
import CloseTabIcon from './CloseTabIcon';
import DraftTabIcon from './DraftTabIcon';
import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons';
const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick }) => {
const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDraft }) => {
const getTabInfo = (type, tabName) => {
switch (type) {
case 'collection-settings': {
@@ -60,7 +61,7 @@ const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick }) => {
<>
<div className="flex items-center tab-label pl-2">{getTabInfo(type, tabName)}</div>
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
<CloseTabIcon />
{hasDraft ? <DraftTabIcon /> : <CloseTabIcon />}
</div>
</>
);

View File

@@ -1,14 +1,16 @@
import React, { useCallback, useState, useRef, Fragment, useMemo } from 'react';
import get from 'lodash/get';
import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections';
import { saveRequest, saveCollectionRoot, saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import { deleteRequestDraft, deleteCollectionDraft, deleteFolderDraft } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import darkTheme from 'themes/dark';
import lightTheme from 'themes/light';
import { findItemInCollection, hasRequestChanges } from 'utils/collections';
import ConfirmRequestClose from './ConfirmRequestClose';
import ConfirmCollectionClose from './ConfirmCollectionClose';
import ConfirmFolderClose from './ConfirmFolderClose';
import RequestTabNotFound from './RequestTabNotFound';
import SpecialTab from './SpecialTab';
import StyledWrapper from './StyledWrapper';
@@ -26,6 +28,8 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
const { storedTheme } = useTheme();
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
const [showConfirmClose, setShowConfirmClose] = useState(false);
const [showConfirmCollectionClose, setShowConfirmCollectionClose] = useState(false);
const [showConfirmFolderClose, setShowConfirmFolderClose] = useState(false);
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
@@ -77,17 +81,97 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
return colorMap[method.toLocaleLowerCase()];
};
const handleCloseCollectionSettings = (event) => {
if (!collection.draft) {
return handleCloseClick(event);
}
event.stopPropagation();
event.preventDefault();
setShowConfirmCollectionClose(true);
};
const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
const handleCloseFolderSettings = (event) => {
if (!folder?.draft) {
return handleCloseClick(event);
}
event.stopPropagation();
event.preventDefault();
setShowConfirmFolderClose(true);
};
const hasDraft = tab.type === 'collection-settings' && collection?.draft;
const hasFolderDraft = tab.type === 'folder-settings' && folder?.draft;
if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
return (
<StyledWrapper
className={`flex items-center justify-between tab-container px-1 ${tab.preview ? "italic" : ""}`}
onMouseUp={handleMouseUp} // Add middle-click behavior here
>
{showConfirmCollectionClose && tab.type === 'collection-settings' && (
<ConfirmCollectionClose
collection={collection}
onCancel={() => setShowConfirmCollectionClose(false)}
onCloseWithoutSave={() => {
dispatch(deleteCollectionDraft({
collectionUid: collection.uid
}));
dispatch(closeTabs({
tabUids: [tab.uid]
}));
setShowConfirmCollectionClose(false);
}}
onSaveAndClose={() => {
dispatch(saveCollectionRoot(collection.uid))
.then(() => {
dispatch(closeTabs({
tabUids: [tab.uid]
}));
setShowConfirmCollectionClose(false);
})
.catch((err) => {
console.log('err', err);
});
}}
/>
)}
{showConfirmFolderClose && tab.type === 'folder-settings' && (
<ConfirmFolderClose
folder={folder}
onCancel={() => setShowConfirmFolderClose(false)}
onCloseWithoutSave={() => {
dispatch(deleteFolderDraft({
collectionUid: collection.uid,
folderUid: folder.uid
}));
dispatch(closeTabs({
tabUids: [tab.uid]
}));
setShowConfirmFolderClose(false);
}}
onSaveAndClose={() => {
dispatch(saveFolderRoot(collection.uid, folder.uid))
.then(() => {
dispatch(closeTabs({
tabUids: [tab.uid]
}));
setShowConfirmFolderClose(false);
})
.catch((err) => {
console.log('err', err);
});
}}
/>
)}
{tab.type === 'folder-settings' && !folder ? (
<RequestTabNotFound handleCloseClick={handleCloseClick} />
) : tab.type === 'folder-settings' ? (
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} />
<SpecialTab handleCloseClick={handleCloseFolderSettings} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} hasDraft={hasFolderDraft} />
) : tab.type === 'collection-settings' ? (
<SpecialTab handleCloseClick={handleCloseCollectionSettings} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={collection?.name} hasDraft={hasDraft} />
) : (
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} />
)}

View File

@@ -9,7 +9,8 @@ const getEffectiveAuthSource = (collection, item) => {
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
if (authMode !== 'inherit') return null;
const collectionAuth = get(collection, 'root.request.auth');
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
uid: collection.uid,

View File

@@ -21,13 +21,14 @@ export const resolveInheritedAuth = (item, collection) => {
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
// Default to collection auth
const collectionAuth = get(collection, 'root.request.auth', { mode: 'none' });
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth', { mode: 'none' });
let effectiveAuth = collectionAuth;
// Check folders in reverse to find the closest auth configuration
for (let i of [...requestTreePath].reverse()) {
if (i.type === 'folder') {
const folderAuth = get(i, 'root.request.auth');
const folderAuth = i?.draft ? get(i, 'draft.request.auth') : get(i, 'root.request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
effectiveAuth = folderAuth;
break;

View File

@@ -2,13 +2,14 @@ import { buildHarRequest } from 'utils/codegenerator/har';
import { getAuthHeaders } from 'utils/codegenerator/auth';
import { getAllVariables, getTreePathFromCollectionToItem } from 'utils/collections/index';
import { interpolateHeaders, interpolateBody } from './interpolation';
import { get } from 'lodash';
// Merge headers from collection, folders, and request
const mergeHeaders = (collection, request, requestTreePath) => {
let headers = new Map();
// Add collection headers first
const collectionHeaders = collection?.root?.request?.headers || [];
const collectionHeaders = collection?.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
collectionHeaders.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header);
@@ -19,7 +20,7 @@ const mergeHeaders = (collection, request, requestTreePath) => {
if (requestTreePath && requestTreePath.length > 0) {
for (let i of requestTreePath) {
if (i.type === 'folder') {
const folderHeaders = i?.root?.request?.headers || [];
const folderHeaders = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'root.request.headers', []);
folderHeaders.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header);
@@ -56,7 +57,7 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
// Add auth headers if needed
if (request.auth && request.auth.mode !== 'none') {
const collectionAuth = collection?.root?.request?.auth || null;
const collectionAuth = collection?.draft?.root ? get(collection, 'draft.root.request.auth', null) : get(collection, 'root.request.auth', null);
const authHeaders = getAuthHeaders(collectionAuth, request.auth);
headers = [...headers, ...authHeaders];
}

View File

@@ -1,11 +1,13 @@
import { useState, useRef, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { browseFiles, updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
import { updateCollectionProtobuf } from 'providers/ReduxStore/slices/collections';
import { getRelativePath, getAbsoluteFilePath } from 'utils/common/path';
import { browseDirectory } from 'utils/filesystem';
import { loadGrpcMethodsFromProtoFile } from 'utils/network/index';
import useLocalStorage from 'hooks/useLocalStorage/index';
import { cloneDeep } from 'lodash';
import get from 'lodash/get';
/**
* Custom hook for managing protofile data and collection configuration
@@ -18,8 +20,13 @@ export default function useProtoFileManagement(collection) {
const [protofileCache, setProtofileCache] = useLocalStorage('bruno.grpc.protofileCache', {});
const [isLoadingMethods, setIsLoadingMethods] = useState(false);
const collectionProtoFiles = useMemo(() => collection?.brunoConfig?.protobuf?.protoFiles || [], [collection?.brunoConfig?.protobuf?.protoFiles]);
const collectionImportPaths = useMemo(() => collection?.brunoConfig?.protobuf?.importPaths || [], [collection?.brunoConfig?.protobuf?.importPaths]);
// Get protobuf config from draft if exists, otherwise from brunoConfig
const protobufConfig = collection?.draft?.brunoConfig
? get(collection, 'draft.brunoConfig.protobuf', {})
: get(collection, 'brunoConfig.protobuf', {});
const collectionProtoFiles = useMemo(() => protobufConfig?.protoFiles || [], [protobufConfig?.protoFiles]);
const collectionImportPaths = useMemo(() => protobufConfig?.importPaths || [], [protobufConfig?.importPaths]);
const protoFilesWithExistence = useMemo(() =>
collectionProtoFiles.map((protoFile) => ({
@@ -78,6 +85,39 @@ export default function useProtoFileManagement(collection) {
return { success: true, relativePath, alreadyExists: true };
}
try {
const protoFileObj = {
path: relativePath,
type: 'file',
exists: true
};
const updatedProtobuf = {
...protobufConfig,
protoFiles: [...collectionProtoFiles, protoFileObj]
};
dispatch(updateCollectionProtobuf({
collectionUid: collection.uid,
protobuf: updatedProtobuf
}));
return { success: true, relativePath };
} catch (error) {
console.error('Error adding proto file to collection:', error);
return { success: false, error };
}
};
const addProtoFileFromRequest = async (filePath) => {
const relativePath = getRelativePath(collection.pathname, filePath, true);
const exists = collectionProtoFiles.some((pf) => pf.path === relativePath);
if (exists) {
return { success: true, relativePath, alreadyExists: true };
}
try {
const protoFileObj = {
path: relativePath,
@@ -104,6 +144,38 @@ export default function useProtoFileManagement(collection) {
};
const addImportPathToCollection = async (directoryPath) => {
const relativePath = getRelativePath(collection.pathname, directoryPath, true);
const importPathObj = {
path: relativePath,
enabled: true,
exists: true
};
const exists = collectionImportPaths.some((ip) => ip.path === importPathObj.path);
if (exists) {
return { success: false, error: new Error('Import path already exists') };
}
try {
const updatedProtobuf = {
...protobufConfig,
importPaths: [...collectionImportPaths, importPathObj]
};
dispatch(updateCollectionProtobuf({
collectionUid: collection.uid,
protobuf: updatedProtobuf
}));
return { success: true, relativePath };
} catch (error) {
console.error('Error adding import path:', error);
return { success: false, error };
}
};
const addImportPathFromRequest = async (directoryPath) => {
const relativePath = getRelativePath(collection.pathname, directoryPath, true);
const importPathObj = {
path: relativePath,
@@ -137,6 +209,34 @@ export default function useProtoFileManagement(collection) {
};
const toggleImportPath = async (index) => {
try {
const updatedImportPaths = [...collectionImportPaths];
updatedImportPaths[index] = {
...updatedImportPaths[index],
enabled: !updatedImportPaths[index].enabled
};
const updatedProtobuf = {
...protobufConfig,
importPaths: updatedImportPaths
};
dispatch(updateCollectionProtobuf({
collectionUid: collection.uid,
protobuf: updatedProtobuf
}));
return {
success: true,
enabled: updatedImportPaths[index].enabled
};
} catch (error) {
console.error('Error toggling import path:', error);
return { success: false, error };
}
};
const toggleImportPathFromRequest = async (index) => {
try {
const updatedImportPaths = [...collectionImportPaths];
updatedImportPaths[index] = {
@@ -195,13 +295,15 @@ export default function useProtoFileManagement(collection) {
const updatedProtoFiles = [...collectionProtoFiles];
updatedProtoFiles.splice(index, 1);
const brunoConfig = cloneDeep(collection.brunoConfig);
if (!brunoConfig.protobuf) {
brunoConfig.protobuf = {};
}
brunoConfig.protobuf.protoFiles = updatedProtoFiles;
const updatedProtobuf = {
...protobufConfig,
protoFiles: updatedProtoFiles
};
await dispatch(updateBrunoConfig(brunoConfig, collection.uid));
dispatch(updateCollectionProtobuf({
collectionUid: collection.uid,
protobuf: updatedProtobuf
}));
return { success: true };
} catch (error) {
@@ -215,13 +317,15 @@ export default function useProtoFileManagement(collection) {
const updatedImportPaths = [...collectionImportPaths];
updatedImportPaths.splice(index, 1);
const brunoConfig = cloneDeep(collection.brunoConfig);
if (!brunoConfig.protobuf) {
brunoConfig.protobuf = {};
}
brunoConfig.protobuf.importPaths = updatedImportPaths;
const updatedProtobuf = {
...protobufConfig,
importPaths: updatedImportPaths
};
await dispatch(updateBrunoConfig(brunoConfig, collection.uid));
dispatch(updateCollectionProtobuf({
collectionUid: collection.uid,
protobuf: updatedProtobuf
}));
return { success: true };
} catch (error) {
@@ -236,16 +340,19 @@ export default function useProtoFileManagement(collection) {
const updatedImportPaths = [...collectionImportPaths];
updatedImportPaths[index] = {
...updatedImportPaths[index],
path: relativePath
path: relativePath,
exists: true
};
const brunoConfig = cloneDeep(collection.brunoConfig);
if (!brunoConfig.protobuf) {
brunoConfig.protobuf = {};
}
brunoConfig.protobuf.importPaths = updatedImportPaths;
const updatedProtobuf = {
...protobufConfig,
importPaths: updatedImportPaths
};
await dispatch(updateBrunoConfig(brunoConfig, collection.uid));
dispatch(updateCollectionProtobuf({
collectionUid: collection.uid,
protobuf: updatedProtobuf
}));
return { success: true };
} catch (error) {
@@ -261,16 +368,19 @@ export default function useProtoFileManagement(collection) {
updatedProtoFiles[index] = {
...updatedProtoFiles[index],
path: relativePath,
type: 'file'
type: 'file',
exists: true
};
const brunoConfig = cloneDeep(collection.brunoConfig);
if (!brunoConfig.protobuf) {
brunoConfig.protobuf = {};
}
brunoConfig.protobuf.protoFiles = updatedProtoFiles;
const updatedProtobuf = {
...protobufConfig,
protoFiles: updatedProtoFiles
};
await dispatch(updateBrunoConfig(brunoConfig, collection.uid));
dispatch(updateCollectionProtobuf({
collectionUid: collection.uid,
protobuf: updatedProtobuf
}));
return { success: true };
} catch (error) {
@@ -286,12 +396,15 @@ export default function useProtoFileManagement(collection) {
loadMethodsFromProtoFile,
addProtoFileToCollection,
addImportPathToCollection,
addImportPathFromRequest,
toggleImportPath,
toggleImportPathFromRequest,
browseForProtoFile,
browseForImportDirectory,
removeProtoFileFromCollection,
removeImportPathFromCollection,
replaceImportPathInCollection,
replaceProtoFileInCollection
replaceProtoFileInCollection,
addProtoFileFromRequest
};
}

View File

@@ -7,55 +7,106 @@ import { useDispatch } from 'react-redux';
import { findCollectionByUid, flattenItems, isItemARequest, hasRequestChanges } from 'utils/collections';
import { pluralizeWord } from 'utils/common';
import { completeQuitFlow } from 'providers/ReduxStore/slices/app';
import { saveMultipleRequests } from 'providers/ReduxStore/slices/collections/actions';
import { saveMultipleRequests, saveMultipleCollections, saveMultipleFolders } from 'providers/ReduxStore/slices/collections/actions';
import { IconAlertTriangle } from '@tabler/icons';
import Modal from 'components/Modal';
const SaveRequestsModal = ({ onClose }) => {
const MAX_UNSAVED_REQUESTS_TO_SHOW = 5;
const MAX_UNSAVED_ITEMS_TO_SHOW = 5;
const collections = useSelector((state) => state.collections.collections);
const tabs = useSelector((state) => state.tabs.tabs);
const dispatch = useDispatch();
const currentDrafts = useMemo(() => {
const drafts = [];
const allDrafts = useMemo(() => {
const requestDrafts = [];
const collectionDrafts = [];
const folderDrafts = [];
const tabsByCollection = groupBy(tabs, (t) => t.collectionUid);
Object.keys(tabsByCollection).forEach((collectionUid) => {
const collection = findCollectionByUid(collections, collectionUid);
if (collection) {
// Check for collection draft
if (collection.draft) {
collectionDrafts.push({
type: 'collection',
name: collection.name,
collectionUid: collectionUid
});
}
// Check for request and folder drafts
const items = flattenItems(collection.items);
const collectionDrafts = filter(items, (item) => isItemARequest(item) && hasRequestChanges(item));
each(collectionDrafts, (draft) => {
drafts.push({
// Request drafts
const requests = filter(items, (item) => isItemARequest(item) && hasRequestChanges(item));
each(requests, (draft) => {
requestDrafts.push({
type: 'request',
...draft,
collectionUid: collectionUid
});
});
// Folder drafts
const folders = filter(items, (item) => item.type === 'folder' && item.draft);
each(folders, (folder) => {
folderDrafts.push({
type: 'folder',
name: folder.name,
folderUid: folder.uid,
collectionUid: collectionUid
});
});
}
});
return drafts;
return [...collectionDrafts, ...folderDrafts, ...requestDrafts];
}, [collections, tabs]);
const totalDraftsCount = allDrafts.length;
useEffect(() => {
if (currentDrafts.length === 0) {
if (totalDraftsCount === 0) {
return dispatch(completeQuitFlow());
}
}, [currentDrafts, dispatch]);
}, [totalDraftsCount, dispatch]);
const closeWithoutSave = () => {
dispatch(completeQuitFlow());
onClose();
};
const closeWithSave = () => {
dispatch(saveMultipleRequests(currentDrafts))
.then(() => dispatch(completeQuitFlow()))
.then(() => onClose());
const closeWithSave = async () => {
try {
// Separate drafts by type
const collectionDrafts = allDrafts.filter((d) => d.type === 'collection');
const folderDrafts = allDrafts.filter((d) => d.type === 'folder');
const requestDrafts = allDrafts.filter((d) => d.type === 'request');
// Save all collection drafts
if (collectionDrafts.length > 0) {
await dispatch(saveMultipleCollections(collectionDrafts));
}
// Save all folder drafts
if (folderDrafts.length > 0) {
await dispatch(saveMultipleFolders(folderDrafts));
}
// Save all request drafts
if (requestDrafts.length > 0) {
await dispatch(saveMultipleRequests(requestDrafts));
}
dispatch(completeQuitFlow());
onClose();
} catch (error) {
console.error('Error saving drafts:', error);
}
};
if (!currentDrafts.length) {
if (totalDraftsCount === 0) {
return null;
}
@@ -77,23 +128,30 @@ const SaveRequestsModal = ({ onClose }) => {
</div>
<p className="mt-4">
Do you want to save the changes you made to the following{' '}
<span className="font-medium">{currentDrafts.length}</span> {pluralizeWord('request', currentDrafts.length)}?
<span className="font-medium">{totalDraftsCount}</span> {pluralizeWord('item', totalDraftsCount)}?
</p>
<ul className="mt-4">
{currentDrafts.slice(0, MAX_UNSAVED_REQUESTS_TO_SHOW).map((item) => {
{allDrafts.slice(0, MAX_UNSAVED_ITEMS_TO_SHOW).map((item, index) => {
const prefix
= item.type === 'collection'
? 'Collection: '
: item.type === 'folder'
? 'Folder: '
: 'Request: ';
return (
<li key={item.uid} className="mt-1 text-xs">
{item.filename}
<li key={`${item.type}-${item.collectionUid || item.uid}-${index}`} className="mt-1 text-xs">
{prefix}
{item.name || item.filename}
</li>
);
})}
</ul>
{currentDrafts.length > MAX_UNSAVED_REQUESTS_TO_SHOW && (
{totalDraftsCount > MAX_UNSAVED_ITEMS_TO_SHOW && (
<p className="mt-1 text-xs">
...{currentDrafts.length - MAX_UNSAVED_REQUESTS_TO_SHOW} additional{' '}
{pluralizeWord('request', currentDrafts.length - MAX_UNSAVED_REQUESTS_TO_SHOW)} not shown
...{totalDraftsCount - MAX_UNSAVED_ITEMS_TO_SHOW} additional{' '}
{pluralizeWord('item', totalDraftsCount - MAX_UNSAVED_ITEMS_TO_SHOW)} not shown
</p>
)}
@@ -108,7 +166,7 @@ const SaveRequestsModal = ({ onClose }) => {
Cancel
</button>
<button className="btn btn-secondary btn-sm" onClick={closeWithSave}>
{currentDrafts.length > 1 ? 'Save All' : 'Save'}
{totalDraftsCount > 1 ? 'Save All' : 'Save'}
</button>
</div>
</div>

View File

@@ -11,7 +11,8 @@ import {
sendRequest,
saveRequest,
saveCollectionRoot,
saveFolderRoot
saveFolderRoot,
saveCollectionSettings
} from 'providers/ReduxStore/slices/collections/actions';
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
import { closeTabs, reorderTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
@@ -57,7 +58,7 @@ export const HotkeysProvider = (props) => {
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
}
} else if (activeTab.type === 'collection-settings') {
dispatch(saveCollectionRoot(collection.uid));
dispatch(saveCollectionSettings(collection.uid));
}
}
}

View File

@@ -1,6 +1,7 @@
import { handleMakeTabParmanent } from "./utils";
const actionsToIntercept = [
// Request-level actions
'collections/requestUrlChanged',
'collections/updateAuth',
'collections/addQueryParam',
@@ -37,14 +38,39 @@ const actionsToIntercept = [
'collections/updateVar',
'collections/deleteVar',
'collections/moveVar',
'collections/updateRequestDocs',
'collections/runRequestEvent', // TODO: This doesn't necessarily related to a draft state, need to rethink.
// Folder-level actions
'collections/addFolderHeader',
'collections/updateFolderHeader',
'collections/deleteFolderHeader',
'collections/addFolderVar',
'collections/updateFolderVar',
'collections/deleteFolderVar',
'collections/updateRequestDocs',
'collections/runRequestEvent', // TODO: This doesn't necessarily related to a draft state, need to rethink.
'collections/updateFolderRequestScript',
'collections/updateFolderResponseScript',
'collections/updateFolderTests',
'collections/updateFolderAuth',
'collections/updateFolderAuthMode',
'collections/updateFolderDocs',
// Collection-level actions
'collections/addCollectionHeader',
'collections/updateCollectionHeader',
'collections/deleteCollectionHeader',
'collections/addCollectionVar',
'collections/updateCollectionVar',
'collections/deleteCollectionVar',
'collections/updateCollectionAuth',
'collections/updateCollectionAuthMode',
'collections/updateCollectionRequestScript',
'collections/updateCollectionResponseScript',
'collections/updateCollectionTests',
'collections/updateCollectionDocs',
'collections/updateCollectionClientCertificates',
'collections/updateCollectionProtobuf',
'collections/updateCollectionProxy'
];
export const draftDetectMiddleware = ({ dispatch, getState }) => (next) => (action) => {

View File

@@ -6,14 +6,35 @@ function handleMakeTabParmanent(state, action, dispatch) {
const tabs = state.tabs.tabs;
const activeTabUid = state.tabs.activeTabUid;
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
const itemUid = action.payload.itemUid || action.payload.folderUid
const collection = findCollectionByUid(state.collections.collections, action.payload.collectionUid);
if (collection) {
if (!focusedTab || focusedTab.preview !== true) {
return;
}
const { itemUid, folderUid, collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if (!collection) {
return;
}
// Handle request-level changes
if (itemUid) {
const item = findItemInCollection(collection, itemUid);
if (item && focusedTab.preview == true) {
if (item) {
dispatch(makeTabPermanent({ uid: itemUid }));
}
}
// Handle folder-level changes (folder settings tab)
else if (folderUid) {
const folder = findItemInCollection(collection, folderUid);
if (folder) {
dispatch(makeTabPermanent({ uid: folderUid }));
}
} else if (collectionUid) {
// Handle collection-level changes (collection settings tab)
dispatch(makeTabPermanent({ uid: collectionUid }));
}
}
export {

View File

@@ -17,7 +17,8 @@ import {
isItemAFolder,
refreshUidsInItem,
isItemARequest,
transformRequestToSaveToFilesystem
transformRequestToSaveToFilesystem,
transformCollectionRootToSave
} from 'utils/collections';
import { uuid, waitForNextTick } from 'utils/common';
import { cancelNetworkRequest, connectWS, sendGrpcRequest, sendNetworkRequest, sendWsRequest } from 'utils/network/index';
@@ -43,7 +44,9 @@ import {
updateRunnerConfiguration as _updateRunnerConfiguration,
updateActiveConnections,
saveRequest as _saveRequest,
saveEnvironment as _saveEnvironment
saveEnvironment as _saveEnvironment,
saveCollectionDraft,
saveFolderDraft
} from './index';
import { each } from 'lodash';
@@ -58,7 +61,8 @@ import {
getReorderedItemsInTargetDirectory,
resetSequencesInFolder,
getReorderedItemsInSourceDirectory,
calculateDraggedItemNewPathname
calculateDraggedItemNewPathname,
transformFolderRootToSave
} from 'utils/collections/index';
import { sanitizeName } from 'utils/common/regex';
import { buildPersistedEnvVariables } from 'utils/environments';
@@ -160,11 +164,18 @@ export const saveCollectionRoot = (collectionUid) => (dispatch, getState) => {
return reject(new Error('Collection not found'));
}
const collectionCopy = cloneDeep(collection);
// Transform collection root (uses draft if exists)
const collectionRootToSave = transformCollectionRootToSave(collectionCopy);
const { ipcRenderer } = window;
ipcRenderer
.invoke('renderer:save-collection-root', collection.pathname, collection.root)
.then(() => toast.success('Collection Settings saved successfully'))
.invoke('renderer:save-collection-root', collectionCopy.pathname, collectionRootToSave)
.then(() => {
toast.success('Collection Settings saved successfully');
dispatch(saveCollectionDraft({ collectionUid }));
})
.then(resolve)
.catch((err) => {
toast.error('Failed to save collection settings!');
@@ -189,15 +200,107 @@ export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState)
const { ipcRenderer } = window;
// Use draft if it exists, otherwise use root
const folderRootToSave = transformFolderRootToSave(folder);
const folderData = {
name: folder.name,
pathname: folder.pathname,
root: folder.root
root: folderRootToSave
};
ipcRenderer
.invoke('renderer:save-folder-root', folderData)
.then(() => toast.success('Folder Settings saved successfully'))
.then(() => {
toast.success('Folder Settings saved successfully');
// If there was a draft, save it to root and clear the draft
if (folder.draft) {
dispatch(saveFolderDraft({ collectionUid, folderUid }));
}
})
.then(resolve)
.catch((err) => {
toast.error('Failed to save folder settings!');
reject(err);
});
});
};
export const saveMultipleCollections = (collectionDrafts) => (dispatch, getState) => {
const state = getState();
const { collections } = state.collections;
return new Promise((resolve, reject) => {
const savePromises = [];
each(collectionDrafts, (collectionDraft) => {
const collection = findCollectionByUid(collections, collectionDraft.collectionUid);
if (collection) {
const collectionCopy = cloneDeep(collection);
const collectionRootToSave = transformCollectionRootToSave(collectionCopy);
const { ipcRenderer } = window;
let savePromises = [];
savePromises.push(ipcRenderer.invoke('renderer:save-collection-root', collectionCopy.pathname, collectionRootToSave));
if (collectionCopy.draft?.brunoConfig) {
savePromises.push(ipcRenderer.invoke('renderer:update-bruno-config', collectionCopy.draft.brunoConfig, collectionCopy.pathname, collectionDraft.collectionUid));
}
Promise.all(savePromises)
.then(() => {
dispatch(saveCollectionDraft({ collectionUid: collectionDraft.collectionUid }));
})
.catch((err) => {
toast.error('Failed to save collection settings!');
reject(err);
});
}
});
Promise.all(savePromises)
.then(resolve)
.catch((err) => {
toast.error('Failed to save collection settings!');
reject(err);
});
});
};
export const saveMultipleFolders = (folderDrafts) => (dispatch, getState) => {
const state = getState();
const { collections } = state.collections;
return new Promise((resolve, reject) => {
const savePromises = [];
each(folderDrafts, (folderDraft) => {
const collection = findCollectionByUid(collections, folderDraft.collectionUid);
const folder = collection ? findItemInCollection(collection, folderDraft.folderUid) : null;
if (collection && folder) {
const folderRootToSave = transformFolderRootToSave(folder);
const folderData = {
name: folder.name,
pathname: folder.pathname,
root: folderRootToSave
};
const { ipcRenderer } = window;
const savePromise = ipcRenderer
.invoke('renderer:save-folder-root', folderData)
.then(() => {
if (folder.draft) {
dispatch(saveFolderDraft({ collectionUid: folderDraft.collectionUid, folderUid: folderDraft.folderUid }));
}
});
savePromises.push(savePromise);
}
});
Promise.all(savePromises)
.then(resolve)
.catch((err) => {
toast.error('Failed to save folder settings!');
@@ -1642,6 +1745,45 @@ export const browseFiles = (filters, properties) => (_dispatch, _getState) => {
});
};
export const saveCollectionSettings = (collectionUid, brunoConfig = null) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
return new Promise((resolve, reject) => {
if (!collection) {
return reject(new Error('Collection not found'));
}
const collectionCopy = cloneDeep(collection);
// Transform collection root (uses draft if exists)
const collectionRootToSave = transformCollectionRootToSave(collectionCopy);
const { ipcRenderer } = window;
const savePromises = [];
// Save collection.bru file
savePromises.push(ipcRenderer.invoke('renderer:save-collection-root', collectionCopy.pathname, collectionRootToSave));
// Save bruno.json if brunoConfig is provided or if there's a brunoConfig draft
const brunoConfigToSave = brunoConfig || (collectionCopy.draft && collectionCopy.draft.brunoConfig);
if (brunoConfigToSave) {
savePromises.push(ipcRenderer.invoke('renderer:update-bruno-config', brunoConfigToSave, collectionCopy.pathname, collectionUid));
}
Promise.all(savePromises)
.then(() => {
toast.success('Collection Settings saved successfully');
dispatch(saveCollectionDraft({ collectionUid }));
})
.then(resolve)
.catch((err) => {
toast.error('Failed to save collection settings!');
reject(err);
});
});
};
export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getState) => {
const state = getState();

View File

@@ -637,6 +637,43 @@ export const collectionsSlice = createSlice({
}
}
},
saveCollectionDraft: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection && collection.draft) {
if (collection.draft.root) {
collection.root = collection.draft.root;
}
if (collection.draft.brunoConfig) {
collection.brunoConfig = collection.draft.brunoConfig;
}
collection.draft = null;
}
},
saveFolderDraft: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
if (folder && folder.draft) {
folder.root = folder.draft;
folder.draft = null;
}
},
deleteCollectionDraft: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection && collection.draft) {
collection.draft = null;
}
},
deleteFolderDraft: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
if (folder && folder.draft) {
folder.draft = null;
}
},
newEphemeralHttpRequest: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@@ -1142,7 +1179,13 @@ export const collectionsSlice = createSlice({
return;
}
collection.root.request.headers = map(headers, ({name = '', value = '', enabled = true}) => ({
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root)
};
}
collection.draft.root.request.headers = map(headers, ({ name = '', value = '', enabled = true }) => ({
uid: uuid(),
name: name,
value: value,
@@ -1797,40 +1840,50 @@ export const collectionsSlice = createSlice({
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
set(collection, 'root.request.auth', {});
set(collection, 'root.request.auth.mode', action.payload.mode);
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root)
};
}
set(collection, 'draft.root.request.auth', {});
set(collection, 'draft.root.request.auth.mode', action.payload.mode);
}
},
updateCollectionAuth: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
set(collection, 'root.request.auth', {});
set(collection, 'root.request.auth.mode', action.payload.mode);
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root)
};
}
set(collection, 'draft.root.request.auth', {});
set(collection, 'draft.root.request.auth.mode', action.payload.mode);
switch (action.payload.mode) {
case 'awsv4':
set(collection, 'root.request.auth.awsv4', action.payload.content);
set(collection, 'draft.root.request.auth.awsv4', action.payload.content);
break;
case 'bearer':
set(collection, 'root.request.auth.bearer', action.payload.content);
set(collection, 'draft.root.request.auth.bearer', action.payload.content);
break;
case 'basic':
set(collection, 'root.request.auth.basic', action.payload.content);
set(collection, 'draft.root.request.auth.basic', action.payload.content);
break;
case 'digest':
set(collection, 'root.request.auth.digest', action.payload.content);
set(collection, 'draft.root.request.auth.digest', action.payload.content);
break;
case 'ntlm':
set(collection, 'root.request.auth.ntlm', action.payload.content);
set(collection, 'draft.root.request.auth.ntlm', action.payload.content);
break;
case 'oauth2':
set(collection, 'root.request.auth.oauth2', action.payload.content);
set(collection, 'draft.root.request.auth.oauth2', action.payload.content);
break;
case 'wsse':
set(collection, 'root.request.auth.wsse', action.payload.content);
set(collection, 'draft.root.request.auth.wsse', action.payload.content);
break;
case 'apikey':
set(collection, 'root.request.auth.apikey', action.payload.content);
set(collection, 'draft.root.request.auth.apikey', action.payload.content);
break;
}
}
@@ -1839,35 +1892,122 @@ export const collectionsSlice = createSlice({
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
set(collection, 'root.request.script.req', action.payload.script);
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root)
};
}
set(collection, 'draft.root.request.script.req', action.payload.script);
}
},
updateCollectionResponseScript: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
set(collection, 'root.request.script.res', action.payload.script);
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root)
};
}
set(collection, 'draft.root.request.script.res', action.payload.script);
}
},
updateCollectionTests: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
set(collection, 'root.request.tests', action.payload.tests);
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root)
};
}
set(collection, 'draft.root.request.tests', action.payload.tests);
}
},
updateCollectionDocs: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
set(collection, 'root.docs', action.payload.docs);
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root)
};
}
set(collection, 'draft.root.docs', action.payload.docs);
}
},
updateCollectionProxy: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root),
brunoConfig: cloneDeep(collection.brunoConfig)
};
}
if (!collection.draft.brunoConfig) {
collection.draft.brunoConfig = cloneDeep(collection.brunoConfig);
}
set(collection, 'draft.brunoConfig.proxy', action.payload.proxy);
}
},
updateCollectionClientCertificates: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root),
brunoConfig: cloneDeep(collection.brunoConfig)
};
}
if (!collection.draft.brunoConfig) {
collection.draft.brunoConfig = cloneDeep(collection.brunoConfig);
}
set(collection, 'draft.brunoConfig.clientCertificates', action.payload.clientCertificates);
}
},
updateCollectionPresets: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root),
brunoConfig: cloneDeep(collection.brunoConfig)
};
}
if (!collection.draft.brunoConfig) {
collection.draft.brunoConfig = cloneDeep(collection.brunoConfig);
}
set(collection, 'draft.brunoConfig.presets', action.payload.presets);
}
},
updateCollectionProtobuf: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root),
brunoConfig: cloneDeep(collection.brunoConfig)
};
}
if (!collection.draft.brunoConfig) {
collection.draft.brunoConfig = cloneDeep(collection.brunoConfig);
}
set(collection, 'draft.brunoConfig.protobuf', action.payload.protobuf);
}
},
addFolderHeader: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
if (folder) {
const headers = get(folder, 'root.request.headers', []);
if (!folder.draft) {
folder.draft = cloneDeep(folder.root);
}
const headers = get(folder, 'draft.request.headers', []);
headers.push({
uid: uuid(),
name: '',
@@ -1875,14 +2015,17 @@ export const collectionsSlice = createSlice({
description: '',
enabled: true
});
set(folder, 'root.request.headers', headers);
set(folder, 'draft.request.headers', headers);
}
},
updateFolderHeader: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
if (folder) {
const headers = get(folder, 'root.request.headers', []);
if (!folder.draft) {
folder.draft = cloneDeep(folder.root);
}
const headers = get(folder, 'draft.request.headers', []);
const header = find(headers, (h) => h.uid === action.payload.header.uid);
if (header) {
header.name = action.payload.header.name;
@@ -1896,9 +2039,12 @@ export const collectionsSlice = createSlice({
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
if (folder) {
let headers = get(folder, 'root.request.headers', []);
if (!folder.draft) {
folder.draft = cloneDeep(folder.root);
}
let headers = get(folder, 'draft.request.headers', []);
headers = filter(headers, (h) => h.uid !== action.payload.headerUid);
set(folder, 'root.request.headers', headers);
set(folder, 'draft.request.headers', headers);
}
},
addFolderVar: (state, action) => {
@@ -1906,24 +2052,27 @@ export const collectionsSlice = createSlice({
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
const type = action.payload.type;
if (folder) {
if (!folder.draft) {
folder.draft = cloneDeep(folder.root);
}
if (type === 'request') {
const vars = get(folder, 'root.request.vars.req', []);
const vars = get(folder, 'draft.request.vars.req', []);
vars.push({
uid: uuid(),
name: '',
value: '',
enabled: true
});
set(folder, 'root.request.vars.req', vars);
set(folder, 'draft.request.vars.req', vars);
} else if (type === 'response') {
const vars = get(folder, 'root.request.vars.res', []);
const vars = get(folder, 'draft.request.vars.res', []);
vars.push({
uid: uuid(),
name: '',
value: '',
enabled: true
});
set(folder, 'root.request.vars.res', vars);
set(folder, 'draft.request.vars.res', vars);
}
}
},
@@ -1932,8 +2081,11 @@ export const collectionsSlice = createSlice({
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
const type = action.payload.type;
if (folder) {
if (!folder.draft) {
folder.draft = cloneDeep(folder.root);
}
if (type === 'request') {
let vars = get(folder, 'root.request.vars.req', []);
let vars = get(folder, 'draft.request.vars.req', []);
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
if (_var) {
_var.name = action.payload.var.name;
@@ -1941,9 +2093,9 @@ export const collectionsSlice = createSlice({
_var.description = action.payload.var.description;
_var.enabled = action.payload.var.enabled;
}
set(folder, 'root.request.vars.req', vars);
set(folder, 'draft.request.vars.req', vars);
} else if (type === 'response') {
let vars = get(folder, 'root.request.vars.res', []);
let vars = get(folder, 'draft.request.vars.res', []);
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
if (_var) {
_var.name = action.payload.var.name;
@@ -1951,7 +2103,7 @@ export const collectionsSlice = createSlice({
_var.description = action.payload.var.description;
_var.enabled = action.payload.var.enabled;
}
set(folder, 'root.request.vars.res', vars);
set(folder, 'draft.request.vars.res', vars);
}
}
},
@@ -1960,14 +2112,17 @@ export const collectionsSlice = createSlice({
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
const type = action.payload.type;
if (folder) {
if (!folder.draft) {
folder.draft = cloneDeep(folder.root);
}
if (type === 'request') {
let vars = get(folder, 'root.request.vars.req', []);
let vars = get(folder, 'draft.request.vars.req', []);
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
set(folder, 'root.request.vars.req', vars);
set(folder, 'draft.request.vars.req', vars);
} else if (type === 'response') {
let vars = get(folder, 'root.request.vars.res', []);
let vars = get(folder, 'draft.request.vars.res', []);
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
set(folder, 'root.request.vars.res', vars);
set(folder, 'draft.request.vars.res', vars);
}
}
},
@@ -1975,21 +2130,30 @@ export const collectionsSlice = createSlice({
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
if (folder) {
set(folder, 'root.request.script.req', action.payload.script);
if (!folder.draft) {
folder.draft = cloneDeep(folder.root);
}
set(folder, 'draft.request.script.req', action.payload.script);
}
},
updateFolderResponseScript: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
if (folder) {
set(folder, 'root.request.script.res', action.payload.script);
if (!folder.draft) {
folder.draft = cloneDeep(folder.root);
}
set(folder, 'draft.request.script.res', action.payload.script);
}
},
updateFolderTests: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
if (folder) {
set(folder, 'root.request.tests', action.payload.tests);
if (!folder.draft) {
folder.draft = cloneDeep(folder.root);
}
set(folder, 'draft.request.tests', action.payload.tests);
}
},
updateFolderAuth: (state, action) => {
@@ -2000,35 +2164,38 @@ export const collectionsSlice = createSlice({
if (!folder) return;
if (folder) {
set(folder, 'root.request.auth', {});
set(folder, 'root.request.auth.mode', action.payload.mode);
if (!folder.draft) {
folder.draft = cloneDeep(folder.root);
}
set(folder, 'draft.request.auth', {});
set(folder, 'draft.request.auth.mode', action.payload.mode);
switch (action.payload.mode) {
case 'oauth2':
set(folder, 'root.request.auth.oauth2', action.payload.content);
set(folder, 'draft.request.auth.oauth2', action.payload.content);
break;
case 'basic':
set(folder, 'root.request.auth.basic', action.payload.content);
set(folder, 'draft.request.auth.basic', action.payload.content);
break;
case 'bearer':
set(folder, 'root.request.auth.bearer', action.payload.content);
set(folder, 'draft.request.auth.bearer', action.payload.content);
break;
case 'digest':
set(folder, 'root.request.auth.digest', action.payload.content);
set(folder, 'draft.request.auth.digest', action.payload.content);
break;
case 'ntlm':
set(folder, 'root.request.auth.ntlm', action.payload.content);
set(folder, 'draft.request.auth.ntlm', action.payload.content);
break;
case 'apikey':
set(folder, 'root.request.auth.apikey', action.payload.content);
set(folder, 'draft.request.auth.apikey', action.payload.content);
break;
case 'awsv4':
set(folder, 'root.request.auth.awsv4', action.payload.content);
set(folder, 'draft.request.auth.awsv4', action.payload.content);
break;
case 'wsse':
set(folder, 'root.request.auth.wsse', action.payload.content);
set(folder, 'draft.request.auth.wsse', action.payload.content);
break;
case 'ws':
set(folder, 'root.request.auth.ws', action.payload.content);
set(folder, 'draft.request.auth.ws', action.payload.content);
break;
}
}
@@ -2037,7 +2204,12 @@ export const collectionsSlice = createSlice({
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const headers = get(collection, 'root.request.headers', []);
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root)
};
}
const headers = get(collection, 'draft.root.request.headers', []);
headers.push({
uid: uuid(),
name: '',
@@ -2045,14 +2217,19 @@ export const collectionsSlice = createSlice({
description: '',
enabled: true
});
set(collection, 'root.request.headers', headers);
set(collection, 'draft.root.request.headers', headers);
}
},
updateCollectionHeader: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const headers = get(collection, 'root.request.headers', []);
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root)
};
}
const headers = get(collection, 'draft.root.request.headers', []);
const header = find(headers, (h) => h.uid === action.payload.header.uid);
if (header) {
header.name = action.payload.header.name;
@@ -2066,73 +2243,95 @@ export const collectionsSlice = createSlice({
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
let headers = get(collection, 'root.request.headers', []);
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root)
};
}
let headers = get(collection, 'draft.root.request.headers', []);
headers = filter(headers, (h) => h.uid !== action.payload.headerUid);
set(collection, 'root.request.headers', headers);
set(collection, 'draft.root.request.headers', headers);
}
},
addCollectionVar: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const type = action.payload.type;
if (collection) {
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root)
};
}
if (type === 'request') {
const vars = get(collection, 'root.request.vars.req', []);
const vars = get(collection, 'draft.root.request.vars.req', []);
vars.push({
uid: uuid(),
name: '',
value: '',
enabled: true
});
set(collection, 'root.request.vars.req', vars);
set(collection, 'draft.root.request.vars.req', vars);
} else if (type === 'response') {
const vars = get(collection, 'root.request.vars.res', []);
const vars = get(collection, 'draft.root.request.vars.res', []);
vars.push({
uid: uuid(),
name: '',
value: '',
enabled: true
});
set(collection, 'root.request.vars.res', vars);
set(collection, 'draft.root.request.vars.res', vars);
}
}
},
updateCollectionVar: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const type = action.payload.type;
if (type === 'request') {
let vars = get(collection, 'root.request.vars.req', []);
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
if (_var) {
_var.name = action.payload.var.name;
_var.value = action.payload.var.value;
_var.description = action.payload.var.description;
_var.enabled = action.payload.var.enabled;
if (collection) {
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root)
};
}
set(collection, 'root.request.vars.req', vars);
} else if (type === 'response') {
let vars = get(collection, 'root.request.vars.res', []);
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
if (_var) {
_var.name = action.payload.var.name;
_var.value = action.payload.var.value;
_var.description = action.payload.var.description;
_var.enabled = action.payload.var.enabled;
if (type === 'request') {
let vars = get(collection, 'draft.root.request.vars.req', []);
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
if (_var) {
_var.name = action.payload.var.name;
_var.value = action.payload.var.value;
_var.description = action.payload.var.description;
_var.enabled = action.payload.var.enabled;
}
set(collection, 'draft.root.request.vars.req', vars);
} else if (type === 'response') {
let vars = get(collection, 'draft.root.request.vars.res', []);
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
if (_var) {
_var.name = action.payload.var.name;
_var.value = action.payload.var.value;
_var.description = action.payload.var.description;
_var.enabled = action.payload.var.enabled;
}
set(collection, 'draft.root.request.vars.res', vars);
}
set(collection, 'root.request.vars.res', vars);
}
},
deleteCollectionVar: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const type = action.payload.type;
if (collection) {
if (!collection.draft) {
collection.draft = {
root: cloneDeep(collection.root)
};
}
if (type === 'request') {
let vars = get(collection, 'root.request.vars.req', []);
let vars = get(collection, 'draft.root.request.vars.req', []);
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
set(collection, 'root.request.vars.req', vars);
set(collection, 'draft.root.request.vars.req', vars);
} else if (type === 'response') {
let vars = get(collection, 'root.request.vars.res', []);
let vars = get(collection, 'draft.root.request.vars.res', []);
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
set(collection, 'root.request.vars.res', vars);
set(collection, 'draft.root.request.vars.res', vars);
}
}
},
@@ -2636,7 +2835,10 @@ export const collectionsSlice = createSlice({
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
if (folder) {
if (isItemAFolder(folder)) {
set(folder, 'root.docs', action.payload.docs);
if (!folder.draft) {
folder.draft = cloneDeep(folder.root);
}
set(folder, 'draft.docs', action.payload.docs);
}
}
},
@@ -2727,8 +2929,11 @@ export const collectionsSlice = createSlice({
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
if (folder) {
set(folder, 'root.request.auth', {});
set(folder, 'root.request.auth.mode', action.payload.mode);
if (!folder.draft) {
folder.draft = cloneDeep(folder.root);
}
set(folder, 'draft.request.auth', {});
set(folder, 'draft.request.auth.mode', action.payload.mode);
}
},
@@ -2998,6 +3203,10 @@ export const {
clearRequestTimeline,
saveRequest,
deleteRequestDraft,
saveCollectionDraft,
saveFolderDraft,
deleteCollectionDraft,
deleteFolderDraft,
newEphemeralHttpRequest,
collapseFullCollection,
toggleCollection,
@@ -3068,6 +3277,10 @@ export const {
updateCollectionResponseScript,
updateCollectionTests,
updateCollectionDocs,
updateCollectionProxy,
updateCollectionClientCertificates,
updateCollectionPresets,
updateCollectionProtobuf,
collectionAddFileEvent,
collectionAddDirectoryEvent,
collectionChangeFileEvent,

View File

@@ -748,6 +748,60 @@ export const transformRequestToSaveToFilesystem = (item) => {
return itemToSave;
};
export const transformCollectionRootToSave = (collection) => {
const _collection = collection.draft?.root ? collection.draft.root : collection.root;
const collectionRootToSave = {
docs: _collection?.docs,
meta: _collection?.meta,
request: {
auth: _collection?.request?.auth,
headers: [],
script: _collection?.request?.script,
vars: _collection?.request?.vars,
tests: _collection?.request?.tests
}
};
each(_collection?.request?.headers, (header) => {
collectionRootToSave.request.headers.push({
uid: header.uid,
name: header.name,
value: header.value,
description: header.description,
enabled: header.enabled
});
});
return collectionRootToSave;
};
export const transformFolderRootToSave = (folder) => {
const _folder = folder.draft ? folder.draft : folder.root;
const folderRootToSave = {
docs: _folder.docs,
request: {
auth: _folder?.request?.auth,
headers: [],
script: _folder?.request?.script,
vars: _folder?.request?.vars,
tests: _folder?.request?.tests
}
};
each(_folder.request.headers, (header) => {
folderRootToSave.request.headers.push({
uid: header.uid,
name: header.name,
value: header.value,
description: header.description,
enabled: header.enabled
});
});
return folderRootToSave;
};
// todo: optimize this
export const deleteItemInCollection = (itemUid, collection) => {
collection.items = filter(collection.items, (i) => i.uid !== itemUid);
@@ -1177,7 +1231,8 @@ const mergeVars = (collection, requestTreePath = []) => {
let collectionVariables = {};
let folderVariables = {};
let requestVariables = {};
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
const collectionRoot = collection?.draft?.root || collection?.root || {};
let collectionRequestVars = get(collectionRoot, 'request.vars.req', []);
collectionRequestVars.forEach((_var) => {
if (_var.enabled) {
collectionVariables[_var.name] = _var.value;

View File

@@ -14,7 +14,7 @@ const STREAMING_FILE_SIZE_THRESHOLD = 20 * 1024 * 1024; // 20MB
const prepareRequest = async (item = {}, collection = {}) => {
const request = item?.request;
const brunoConfig = get(collection, 'brunoConfig', {});
const brunoConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig', {}) : get(collection, 'brunoConfig', {});
const collectionPath = collection?.pathname;
const headers = {};
let contentTypeDefined = false;
@@ -48,7 +48,8 @@ const prepareRequest = async (item = {}, collection = {}) => {
responseType: 'arraybuffer'
};
const collectionAuth = get(collection, 'root.request.auth');
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
if (collectionAuth && request.auth?.mode === 'inherit') {
if (collectionAuth.mode === 'basic') {
axiosRequest.basicAuth = {

View File

@@ -123,7 +123,8 @@ const getFolderRoot = (dir) => {
const mergeHeaders = (collection, request, requestTreePath) => {
let headers = new Map();
let collectionHeaders = get(collection, 'root.request.headers', []);
const collectionRoot = collection?.draft?.root || collection?.root || {};
let collectionHeaders = get(collectionRoot, 'request.headers', []);
collectionHeaders.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
@@ -132,7 +133,8 @@ const mergeHeaders = (collection, request, requestTreePath) => {
for (let i of requestTreePath) {
if (i.type === 'folder') {
let _headers = get(i, 'root.request.headers', []);
const folderRoot = i?.draft || i?.root;
let _headers = get(folderRoot, 'request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
@@ -153,7 +155,8 @@ const mergeHeaders = (collection, request, requestTreePath) => {
const mergeVars = (collection, request, requestTreePath) => {
let reqVars = new Map();
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
const collectionRoot = collection?.draft?.root || collection?.root || {};
let collectionRequestVars = get(collectionRoot, 'request.vars.req', []);
let collectionVariables = {};
collectionRequestVars.forEach((_var) => {
if (_var.enabled) {
@@ -165,7 +168,8 @@ const mergeVars = (collection, request, requestTreePath) => {
let requestVariables = {};
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.req', []);
const folderRoot = i?.draft || i?.root;
let vars = get(folderRoot, 'request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
@@ -197,7 +201,7 @@ const mergeVars = (collection, request, requestTreePath) => {
}
let resVars = new Map();
let collectionResponseVars = get(collection, 'root.request.vars.res', []);
let collectionResponseVars = get(collectionRoot, 'request.vars.res', []);
collectionResponseVars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
@@ -205,7 +209,8 @@ const mergeVars = (collection, request, requestTreePath) => {
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.res', []);
const folderRoot = i?.draft || i?.root;
let vars = get(folderRoot, 'request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
@@ -232,26 +237,28 @@ const mergeVars = (collection, request, requestTreePath) => {
};
const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
let collectionPreReqScript = get(collection, 'root.request.script.req', '');
let collectionPostResScript = get(collection, 'root.request.script.res', '');
let collectionTests = get(collection, 'root.request.tests', '');
const collectionRoot = collection?.draft?.root || collection?.root || {};
let collectionPreReqScript = get(collectionRoot, 'request.script.req', '');
let collectionPostResScript = get(collectionRoot, 'request.script.res', '');
let collectionTests = get(collectionRoot, 'request.tests', '');
let combinedPreReqScript = [];
let combinedPostResScript = [];
let combinedTests = [];
for (let i of requestTreePath) {
if (i.type === 'folder') {
let preReqScript = get(i, 'root.request.script.req', '');
const folderRoot = i?.draft || i?.root;
let preReqScript = get(folderRoot, 'request.script.req', '');
if (preReqScript && preReqScript.trim() !== '') {
combinedPreReqScript.push(preReqScript);
}
let postResScript = get(i, 'root.request.script.res', '');
let postResScript = get(folderRoot, 'request.script.res', '');
if (postResScript && postResScript.trim() !== '') {
combinedPostResScript.push(postResScript);
}
let tests = get(i, 'root.request.tests', '');
let tests = get(folderRoot, 'request.tests', '');
if (tests && tests?.trim?.() !== '') {
combinedTests.push(tests);
}
@@ -320,12 +327,14 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
};
const mergeAuth = (collection, request, requestTreePath) => {
let collectionAuth = collection?.root?.request?.auth || { mode: 'none' };
const collectionRoot = collection?.draft?.root || collection?.root || {};
let collectionAuth = collectionRoot?.request?.auth || { mode: 'none' };
let effectiveAuth = collectionAuth;
for (let i of requestTreePath) {
if (i.type === 'folder') {
const folderAuth = i?.root?.request?.auth;
const folderRoot = i?.draft || i?.root;
const folderAuth = get(folderRoot, 'request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
effectiveAuth = folderAuth;
}

View File

@@ -1062,6 +1062,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
const certsAndProxyConfig = await getCertsAndProxyConfig({
collectionUid,
collection,
request: requestCopy,
envVars,
runtimeVariables,
@@ -1195,6 +1196,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const certsAndProxyConfig = await getCertsAndProxyConfig({
collectionUid,
collection,
request: requestCopy,
envVars,
runtimeVariables,

View File

@@ -11,6 +11,7 @@ const { interpolateString } = require('./interpolate-string');
*/
const getCertsAndProxyConfig = async ({
collectionUid,
collection,
request,
envVars,
runtimeVariables,
@@ -40,7 +41,7 @@ const getCertsAndProxyConfig = async ({
httpsAgentRequestFields['caCertificatesCount'] = caCertificatesCount;
httpsAgentRequestFields['ca'] = caCertificates || [];
const brunoConfig = getBrunoConfig(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid, collection);
const interpolationOptions = {
globalEnvironmentVariables,
envVars,

View File

@@ -42,6 +42,7 @@ const registerGrpcEventHandlers = (window) => {
// Get certificates and proxy configuration
const certsAndProxyConfig = await getCertsAndProxyConfig({
collectionUid: collection.uid,
collection,
request: requestCopy.request,
envVars: preparedRequest.envVars,
runtimeVariables,
@@ -174,6 +175,7 @@ const registerGrpcEventHandlers = (window) => {
// Get certificates and proxy configuration
const certsAndProxyConfig = await getCertsAndProxyConfig({
collectionUid: collection.uid,
collection,
request: requestCopy.request,
envVars: preparedRequest.envVars,
runtimeVariables,
@@ -288,7 +290,7 @@ const registerGrpcEventHandlers = (window) => {
caCertFilePath = preferencesUtil.getCustomCaCertificateFilePath();
}
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
const clientCertConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.clientCertificates.certs', []) : get(collection, 'brunoConfig.clientCertificates.certs', []);
for (let clientCert of clientCertConfig) {
const domain = interpolateString(clientCert?.domain, interpolationOptions);

View File

@@ -71,6 +71,7 @@ const getJsSandboxRuntime = (collection) => {
const configureRequest = async (
collectionUid,
collection,
request,
envVars,
runtimeVariables,
@@ -85,6 +86,7 @@ const configureRequest = async (
const certsAndProxyConfig = await getCertsAndProxyConfig({
collectionUid,
collection,
request,
envVars,
runtimeVariables,
@@ -297,7 +299,7 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col
}
);
const collectionRoot = get(collection, 'root', {});
const collectionRoot = collection?.draft?.root || collection?.root || {};
const request = prepareGqlIntrospectionRequest(endpoint, resolvedVars, _request, collectionRoot);
// Get timeout from request settings, resolve inheritance if needed
@@ -314,6 +316,7 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col
const axiosInstance = await configureRequest(
collection.uid,
collection,
request,
envVars,
collection.runtimeVariables,
@@ -592,7 +595,7 @@ const registerNetworkIpc = (mainWindow) => {
const abortController = new AbortController();
const request = await prepareRequest(item, collection, abortController);
request.__bruno__executionMode = 'standalone';
const brunoConfig = getBrunoConfig(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid, collection);
const scriptingConfig = get(brunoConfig, 'scripts', {});
scriptingConfig.runtime = getJsSandboxRuntime(collection);
@@ -641,6 +644,7 @@ const registerNetworkIpc = (mainWindow) => {
}
const axiosInstance = await configureRequest(
collectionUid,
collection,
request,
envVars,
runtimeVariables,
@@ -950,7 +954,7 @@ const registerNetworkIpc = (mainWindow) => {
const collectionPath = collection.pathname;
const folderUid = folder ? folder.uid : null;
const cancelTokenUid = uuid();
const brunoConfig = getBrunoConfig(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid, collection);
const scriptingConfig = get(brunoConfig, 'scripts', {});
scriptingConfig.runtime = getJsSandboxRuntime(collection);
const envVars = getEnvVars(environment);
@@ -1165,6 +1169,7 @@ const registerNetworkIpc = (mainWindow) => {
request.signal = abortController.signal;
const axiosInstance = await configureRequest(
collectionUid,
collection,
request,
envVars,
runtimeVariables,

View File

@@ -15,7 +15,7 @@ const processHeaders = (headers) => {
const prepareGrpcRequest = async (item, collection, environment, runtimeVariables, certsAndProxyConfig = {}) => {
const request = item.draft ? item.draft.request : item.request;
const collectionRoot = collection?.draft ? get(collection, 'draft', {}) : get(collection, 'root', {});
const collectionRoot = collection?.draft?.root ? get(collection, 'draft.root', {}) : get(collection, 'root', {});
const headers = {};
const url = request.url;

View File

@@ -305,7 +305,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
const prepareRequest = async (item, collection = {}, abortController) => {
const request = item.draft ? item.draft.request : item.request;
const settings = item.draft?.settings ?? item.settings;
const collectionRoot = collection?.draft ? get(collection, 'draft', {}) : get(collection, 'root', {});
const collectionRoot = collection?.draft?.root ? get(collection, 'draft.root', {}) : get(collection, 'root', {});
const collectionPath = collection?.pathname;
const headers = {};
let contentTypeDefined = false;

View File

@@ -26,8 +26,8 @@ const { setAuthHeaders } = require('./prepare-request');
const prepareWsRequest = async (item, collection, environment, runtimeVariables, certsAndProxyConfig = {}) => {
const request = item.draft ? item.draft.request : item.request;
const collectionRoot = collection?.draft ? get(collection, 'draft', {}) : get(collection, 'root', {});
const brunoConfig = get(collection, 'brunoConfig', {});
const collectionRoot = collection?.draft?.root ? get(collection, 'draft.root', {}) : get(collection, 'root', {});
const brunoConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig', {}) : get(collection, 'brunoConfig', {});
const rawHeaders = cloneDeep(request.headers ?? []);
const headers = {};

View File

@@ -5,7 +5,10 @@
const config = {};
// collectionUid is a hash based on the collection path
const getBrunoConfig = (collectionUid) => {
const getBrunoConfig = (collectionUid, collection) => {
if (collection?.draft?.brunoConfig) {
return collection.draft.brunoConfig;
}
return config[collectionUid] || {};
};

View File

@@ -8,7 +8,7 @@ const { preferencesUtil } = require('../store/preferences');
const mergeHeaders = (collection, request, requestTreePath) => {
let headers = new Map();
let collectionHeaders = get(collection, 'root.request.headers', []);
let collectionHeaders = collection?.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
collectionHeaders.forEach((header) => {
if (header.enabled) {
if (header?.name?.toLowerCase?.() === 'content-type') {
@@ -21,7 +21,8 @@ const mergeHeaders = (collection, request, requestTreePath) => {
for (let i of requestTreePath) {
if (i.type === 'folder') {
let _headers = get(i, 'root.request.headers', []);
const folderRoot = i?.draft || i?.root;
let _headers = get(folderRoot, 'request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
if (header.name.toLowerCase() === 'content-type') {
@@ -50,7 +51,8 @@ const mergeHeaders = (collection, request, requestTreePath) => {
const mergeVars = (collection, request, requestTreePath = []) => {
let reqVars = new Map();
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
const collectionRoot = collection?.draft?.root || collection?.root || {};
let collectionRequestVars = get(collectionRoot, 'request.vars.req', []);
let collectionVariables = {};
collectionRequestVars.forEach((_var) => {
if (_var.enabled) {
@@ -62,7 +64,8 @@ const mergeVars = (collection, request, requestTreePath = []) => {
let requestVariables = {};
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.req', []);
const folderRoot = i?.draft || i?.root;
let vars = get(folderRoot, 'request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
@@ -94,7 +97,7 @@ const mergeVars = (collection, request, requestTreePath = []) => {
}
let resVars = new Map();
let collectionResponseVars = get(collection, 'root.request.vars.res', []);
let collectionResponseVars = get(collectionRoot, 'request.vars.res', []);
collectionResponseVars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
@@ -102,7 +105,8 @@ const mergeVars = (collection, request, requestTreePath = []) => {
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.res', []);
const folderRoot = i?.draft || i?.root;
let vars = get(folderRoot, 'request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
@@ -129,26 +133,28 @@ const mergeVars = (collection, request, requestTreePath = []) => {
};
const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
let collectionPreReqScript = get(collection, 'root.request.script.req', '');
let collectionPostResScript = get(collection, 'root.request.script.res', '');
let collectionTests = get(collection, 'root.request.tests', '');
const collectionRoot = collection?.draft?.root || collection?.root || {};
let collectionPreReqScript = get(collectionRoot, 'request.script.req', '');
let collectionPostResScript = get(collectionRoot, 'request.script.res', '');
let collectionTests = get(collectionRoot, 'request.tests', '');
let combinedPreReqScript = [];
let combinedPostResScript = [];
let combinedTests = [];
for (let i of requestTreePath) {
if (i.type === 'folder') {
let preReqScript = get(i, 'root.request.script.req', '');
const folderRoot = i?.draft || i?.root;
let preReqScript = get(folderRoot, 'request.script.req', '');
if (preReqScript && preReqScript.trim() !== '') {
combinedPreReqScript.push(preReqScript);
}
let postResScript = get(i, 'root.request.script.res', '');
let postResScript = get(folderRoot, 'request.script.res', '');
if (postResScript && postResScript.trim() !== '') {
combinedPostResScript.push(postResScript);
}
let tests = get(i, 'root.request.tests', '');
let tests = get(folderRoot, 'request.tests', '');
if (tests && tests?.trim?.() !== '') {
combinedTests.push(tests);
}
@@ -499,14 +505,16 @@ const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = [] }) =>
const mergeAuth = (collection, request, requestTreePath) => {
// Start with collection level auth (always consider collection auth as base)
let collectionAuth = get(collection, 'root.request.auth', { mode: 'none' });
const collectionRoot = collection?.draft?.root || collection?.root || {};
let collectionAuth = get(collectionRoot, 'request.auth', { mode: 'none' });
let effectiveAuth = collectionAuth;
let lastFolderWithAuth = null;
// Traverse through the path to find the closest auth configuration
for (let i of requestTreePath) {
if (i.type === 'folder') {
const folderAuth = get(i, 'root.request.auth');
const folderRoot = i?.draft || i?.root;
const folderAuth = get(folderRoot, 'request.auth');
// Only consider folders that have a valid auth mode
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
effectiveAuth = folderAuth;

View File

@@ -3,13 +3,13 @@ const { configureRequest } = require('../../src/ipc/network/index');
describe('index: configureRequest', () => {
it("Should add 'http://' to the URL if no protocol is specified", async () => {
const request = { method: 'GET', url: 'test-domain', body: {} };
await configureRequest(null, request, null, null, null, null);
await configureRequest(null, null, request, null, null, null, null);
expect(request.url).toEqual('http://test-domain');
});
it("Should NOT add 'http://' to the URL if a protocol is specified", async () => {
const request = { method: 'GET', url: 'ftp://test-domain', body: {} };
await configureRequest(null, request, null, null, null, null);
await configureRequest(null, null, request, null, null, null, null);
expect(request.url).toEqual('ftp://test-domain');
});
});

View File

@@ -0,0 +1,360 @@
import { test, expect } from '../../../playwright';
import { closeAllCollections, createCollection, openCollectionAndAcceptSandbox } from '../../utils/page';
test.describe('Draft indicator in collection and folder settings', () => {
test.afterAll(async ({ page }) => {
// cleanup: close all collections
await closeAllCollections(page);
});
test('Verify draft indicator appears when changing collection settings - Headers', async ({ page, createTmpDir }) => {
const collectionName = 'test-draft';
// Create a new collection
await createCollection(page, collectionName, await createTmpDir());
// Open collection settings by clicking on the collection name
await openCollectionAndAcceptSandbox(page, collectionName);
// Verify the collection settings tab is open
await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'Collection' })).toBeVisible();
// Verify initially there is NO draft indicator (close icon is present)
const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) });
await expect(collectionTab.locator('.close-icon')).toBeVisible();
await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();
// Click on Headers tab
await page.locator('.tab.headers').click();
// Add a new header
await page.getByRole('button', { name: 'Add Header' }).click();
// Fill in header name and value in the table
// Target the table and get the first row's CodeMirror editors
const headerTable = page.locator('table').first();
const headerRow = headerTable.locator('tbody tr').first();
// Fill in the name field (first CodeMirror in the row)
const nameEditor = headerRow.locator('.CodeMirror').first();
await nameEditor.click();
await page.keyboard.type('X-Custom-Header');
// Fill in the value field (second CodeMirror in the row)
const valueEditor = headerRow.locator('.CodeMirror').nth(1);
await valueEditor.click();
await page.keyboard.type('custom-value');
// Verify draft indicator appears in the tab
await expect(collectionTab.locator('.has-changes-icon')).toBeVisible();
await expect(collectionTab.locator('.close-icon')).not.toBeVisible();
// Save the changes
await page.getByRole('button', { name: 'Save' }).click();
// Verify draft indicator is gone after saving
await expect(collectionTab.locator('.close-icon')).toBeVisible();
await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();
});
test('Verify draft indicator appears when changing collection settings - Auth', async ({ page }) => {
// Verify the collection settings tab is open
const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) });
await expect(collectionTab).toBeVisible();
// Verify initially there is NO draft indicator
await expect(collectionTab.locator('.close-icon')).toBeVisible();
await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();
// Click on Auth tab
await page.locator('.tab.auth').click();
// Change auth mode from 'none' to 'bearer' by clicking the dropdown
await page.locator('.auth-mode-label').click();
await page.locator('.dropdown-item').filter({ hasText: 'Bearer Token' }).click();
// Verify draft indicator appears in the tab
await expect(collectionTab.locator('.has-changes-icon')).toBeVisible();
await expect(collectionTab.locator('.close-icon')).not.toBeVisible();
// Save the changes
await page.getByRole('button', { name: 'Save' }).click();
// Verify draft indicator is gone after saving
await expect(collectionTab.locator('.close-icon')).toBeVisible();
await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();
});
test('Verify draft indicator appears when changing collection settings - Protobuf', async ({ page }) => {
const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) });
await expect(collectionTab).toBeVisible();
// Verify initially there is NO draft indicator
await expect(collectionTab.locator('.close-icon')).toBeVisible();
await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();
// Click on Protobuf tab
await page.locator('.tab.protobuf').click();
// Add a new proto file - handle file picker dialog
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByTestId('protobuf-add-file-button').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles('./tests/collection/draft/fixtures/grpcbin.proto');
// Wait for the file to be processed and added to the table
// The file goes through IPC to get the path, then Redux to update state
const protoFilesTable = page.getByTestId('protobuf-proto-file-name');
await expect(protoFilesTable.getByText('grpcbin.proto')).toBeVisible();
// Verify draft indicator appears
await expect(collectionTab.locator('.has-changes-icon')).toBeVisible();
await expect(collectionTab.locator('.close-icon')).not.toBeVisible();
// Save the changes
await page.getByRole('button', { name: 'Save' }).click();
// Verify draft indicator is gone after saving
await expect(collectionTab.locator('.close-icon')).toBeVisible();
await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();
});
test('Verify draft indicator appears when changing client certificate settings', async ({ page }) => {
const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) });
await expect(collectionTab).toBeVisible();
// Verify initially there is NO draft indicator
await expect(collectionTab.locator('.close-icon')).toBeVisible();
await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();
// Click on Client Certificates tab
await page.locator('.tab.clientCert').click();
// Fill domain
await page.locator('#domain').fill('test.com');
// Select cert file using file picker (using grpcbin.proto as a dummy file)
const certFileChooserPromise = page.waitForEvent('filechooser');
await page.locator('input#certFilePath[type="file"]').click();
const certFileChooser = await certFileChooserPromise;
await certFileChooser.setFiles('./tests/collection/draft/fixtures/grpcbin.proto');
// Select key file using file picker (using grpcbin.proto as a dummy file)
const keyFileChooserPromise = page.waitForEvent('filechooser');
await page.locator('input#keyFilePath[type="file"]').click();
const keyFileChooser = await keyFileChooserPromise;
await keyFileChooser.setFiles('./tests/collection/draft/fixtures/grpcbin.proto');
// Click Add button
await page.getByRole('button', { name: 'Add' }).click();
// Verify draft indicator appears
await expect(collectionTab.locator('.has-changes-icon')).toBeVisible();
await expect(collectionTab.locator('.close-icon')).not.toBeVisible();
// Save the changes
await page.getByRole('button', { name: 'Save' }).click();
// Verify draft indicator is gone after saving
await expect(collectionTab.locator('.close-icon')).toBeVisible();
await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();
});
test('Verify draft indicator appears when changing proxy settings', async ({ page }) => {
const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) });
await expect(collectionTab).toBeVisible();
// Verify initially there is NO draft indicator
await expect(collectionTab.locator('.close-icon')).toBeVisible();
await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();
// Click on Proxy tab
await page.locator('.tab.proxy').click();
// Enable proxy - select "enabled" radio button
await page.locator('input[name="enabled"][value="true"]').check();
// Fill in hostname and port
await page.locator('#hostname').fill('localhost');
await page.locator('#port').fill('8080');
// Verify draft indicator appears
await expect(collectionTab.locator('.has-changes-icon')).toBeVisible();
await expect(collectionTab.locator('.close-icon')).not.toBeVisible();
// Save the changes
await page.getByRole('button', { name: 'Save' }).click();
// Verify draft indicator is gone after saving
await expect(collectionTab.locator('.close-icon')).toBeVisible();
await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();
});
test('Verify draft indicator appears when changing collection settings - Vars', async ({ page }) => {
const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) });
// Verify initially there is NO draft indicator
await expect(collectionTab.locator('.close-icon')).toBeVisible();
// Click on Vars tab
await page.locator('.tab.vars').click();
// Add a new variable in the Pre Request section
await page.locator('.btn-add-var').first().click();
// Fill in variable name and value in the table
// Target the vars table and get the first row
const varsTable = page.locator('table').first();
const varRow = varsTable.locator('tbody tr').first();
// Fill in the name field (regular input)
const varNameInput = varRow.locator('input[type="text"]');
await varNameInput.click();
await varNameInput.fill('testVar');
// Fill in the value field (CodeMirror editor)
const valueEditor = varRow.locator('.CodeMirror');
await valueEditor.click();
await page.keyboard.type('testValue');
// Verify draft indicator appears
await expect(collectionTab.locator('.has-changes-icon')).toBeVisible();
await expect(collectionTab.locator('.close-icon')).not.toBeVisible();
// Save the changes
await page.getByRole('button', { name: 'Save' }).click();
// Verify draft indicator is gone after saving
await expect(collectionTab.locator('.close-icon')).toBeVisible();
await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();
});
test('Verify draft indicator appears when changing folder settings - Headers', async ({ page }) => {
const collectionName = 'test-draft';
// Create a folder in the collection
const collection = page.locator('.collection-name').filter({ hasText: collectionName });
await collection.locator('.collection-actions').hover();
await collection.locator('.collection-actions .icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();
// Fill folder name
await expect(page.locator('#folder-name')).toBeVisible();
await page.locator('#folder-name').fill('test-folder');
await page.getByRole('button', { name: 'Create' }).click();
await expect(page.locator('.collection-item-name').filter({ hasText: 'test-folder' })).toBeVisible();
// Open folder settings by double-clicking the folder
await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).dblclick();
// Verify folder settings tab is open
const folderTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'test-folder' }) });
await expect(folderTab).toBeVisible();
// Verify initially there is NO draft indicator
await expect(folderTab.locator('.close-icon')).toBeVisible();
await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible();
// Headers tab should be selected by default, add a new header
await page.getByRole('button', { name: 'Add Header' }).click();
// Fill in header name and value in the table
const headerTable = page.locator('table').first();
const headerRow = headerTable.locator('tbody tr').first();
// Fill in the name field (first CodeMirror in the row)
const nameEditor = headerRow.locator('.CodeMirror').first();
await nameEditor.click();
await page.keyboard.type('X-Folder-Header');
// Fill in the value field (second CodeMirror in the row)
const valueEditor = headerRow.locator('.CodeMirror').nth(1);
await valueEditor.click();
await page.keyboard.type('folder-value');
// Verify draft indicator appears in the folder tab
await expect(folderTab.locator('.has-changes-icon')).toBeVisible();
await expect(folderTab.locator('.close-icon')).not.toBeVisible();
// Save the changes
await page.getByRole('button', { name: 'Save' }).click();
// Verify draft indicator is gone after saving
await expect(folderTab.locator('.close-icon')).toBeVisible();
await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible();
});
test('Verify draft indicator appears when changing folder settings - Auth', async ({ page }) => {
// Open folder settings by double-clicking the folder from previous test
await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).dblclick();
// Verify folder settings tab is open
const folderTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'test-folder' }) });
await expect(folderTab).toBeVisible();
// Verify initially no draft indicator
await expect(folderTab.locator('.close-icon')).toBeVisible();
// Click on Auth tab
await page.locator('.tab.auth').click();
// Change auth mode by clicking the dropdown
await page.locator('.auth-mode-label').click();
await page.locator('.dropdown-item').filter({ hasText: 'Bearer Token' }).click();
// Verify draft indicator appears
await expect(folderTab.locator('.has-changes-icon')).toBeVisible();
await expect(folderTab.locator('.close-icon')).not.toBeVisible();
// Save the changes
await page.getByRole('button', { name: 'Save' }).click();
// Verify draft indicator is gone
await expect(folderTab.locator('.close-icon')).toBeVisible();
await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible();
});
test('Verify draft indicator appears when changing folder settings - Vars', async ({ page }) => {
// Open folder settings by double-clicking the folder from previous test
await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).dblclick();
// Verify folder settings tab is open
const folderTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'test-folder' }) });
await expect(folderTab).toBeVisible();
// Verify initially no draft indicator
await expect(folderTab.locator('.close-icon')).toBeVisible();
// Click on Vars tab
await page.locator('.tab.vars').click();
// Add a new variable in the Pre Request section
await page.locator('.btn-add-var').first().click();
// Fill in variable name and value in the table
const varsTable = page.locator('table').first();
const varRow = varsTable.locator('tbody tr').first();
// Fill in the name field (regular input)
const varNameInput = varRow.locator('input[type="text"]');
await varNameInput.click();
await varNameInput.fill('folderVar');
// Fill in the value field (CodeMirror editor)
const valueEditor = varRow.locator('.CodeMirror');
await valueEditor.click();
await page.keyboard.type('folderValue');
// Verify draft indicator appears
await expect(folderTab.locator('.has-changes-icon')).toBeVisible();
await expect(folderTab.locator('.close-icon')).not.toBeVisible();
// Save the changes
await page.getByRole('button', { name: 'Save' }).click();
// Verify draft indicator is gone
await expect(folderTab.locator('.close-icon')).toBeVisible();
await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible();
});
});

View File

@@ -0,0 +1,163 @@
import { test, expect } from '../../../playwright';
import { createCollection, openCollectionAndAcceptSandbox, closeAllCollections } from '../../utils/page';
test.describe('Draft values are used in requests', () => {
test.afterEach(async ({ page }) => {
// cleanup: close all collections
await closeAllCollections(page);
});
test('Verify draft collection headers are used in HTTP requests', async ({ page, createTmpDir }) => {
const collectionName = 'test-draft-headers';
// Create a new collection
await createCollection(page, collectionName, await createTmpDir());
await openCollectionAndAcceptSandbox(page, collectionName);
// Verify the collection settings tab is open
await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'Collection' })).toBeVisible();
// Add collection header in draft (unsaved)
// Click on Headers tab
await page.locator('.tab.headers').click();
// Add a new header
await page.getByRole('button', { name: 'Add Header' }).click();
// Fill in header name and value
const headerTable = page.locator('table').first();
const headerRow = headerTable.locator('tbody tr').first();
// Fill in the name field
const nameEditor = headerRow.locator('.CodeMirror').first();
await nameEditor.click();
await page.keyboard.type('X-Draft-Header');
// Fill in the value field
const valueEditor = headerRow.locator('.CodeMirror').nth(1);
await valueEditor.click();
await page.keyboard.type('draft-value-123');
// Verify draft indicator appears (header is not saved yet)
const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) });
await expect(collectionTab.locator('.has-changes-icon')).toBeVisible();
// Create a folder in the collection
const collection = page.locator('.collection-name').filter({ hasText: collectionName });
await collection.locator('.collection-actions').hover();
await collection.locator('.collection-actions .icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();
await page.locator('#folder-name').fill('Test Folder');
await page.getByRole('button', { name: 'Create', exact: true }).click();
await page.locator('.collection-item-name').filter({ hasText: 'Test Folder' }).click();
// Wait for the folder to be created
await expect(page.locator('.collection-item-name').filter({ hasText: 'Test Folder' })).toBeVisible();
const folder = page.locator('.collection-item-name').filter({ hasText: 'Test Folder' });
// Add a header to the folder
await page.getByRole('button', { name: 'Add Header' }).click();
await nameEditor.click();
await page.keyboard.type('X-Folder-Draft-Header');
await valueEditor.click();
await page.keyboard.type('folder-draft-value-123');
// Create a request in the collection
// Create a new request via collection menu
await folder.locator('.menu-icon').hover();
await folder.locator('.menu-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
// Fill in request details - using httpbin.org which echoes headers back
await page.getByTestId('request-name').fill('Test Request');
await page.getByTestId('new-request-url').locator('.CodeMirror').click();
await page.keyboard.type('https://httpbin.org/headers');
await page.getByRole('button', { name: 'Create', exact: true }).click();
// Send request and verify draft header is included
// Wait for the request tab to be active
await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'Test Request' })).toBeVisible();
// Click on Generate Code from the sidebar request item dropdown
const requestItem = page.locator('.collection-item-name').filter({ hasText: 'Test Request' });
await expect(requestItem).toBeVisible();
// Right-click on the request item to open context menu
await requestItem.click({ button: 'right' });
// Click on Generate Code option
await page.locator('.dropdown-item').filter({ hasText: 'Generate Code' }).click();
// Wait for the Generate Code modal to open
await expect(page.getByTestId('modal-close-button')).toBeVisible();
// Wait for code generator to be visible
const codeGenerator = page.locator('.code-generator');
await expect(codeGenerator).toBeVisible();
// Target the CodeMirror specifically within the code generator modal
const generatedCodeEditor = codeGenerator.locator('.editor-container .CodeMirror').first();
await expect(generatedCodeEditor).toBeVisible();
// Wait for code generation to complete by checking for the URL in the generated code
await expect(generatedCodeEditor).toContainText('https://httpbin.org/headers');
// Check that the generated code contains the draft header
// The header appears as a --header argument in the generated curl/httpie/wget command
await expect(generatedCodeEditor).toContainText('x-draft-header');
await expect(generatedCodeEditor).toContainText('draft-value-123');
await expect(generatedCodeEditor).toContainText('x-folder-draft-header');
await expect(generatedCodeEditor).toContainText('folder-draft-value-123');
// Close the modal by clicking the X button using the test id
await page.getByTestId('modal-close-button').click();
// Wait for modal to fully close before continuing
await page.waitForSelector('.bruno-modal', { state: 'hidden', timeout: 10000 });
await page.waitForSelector('.bruno-modal-backdrop', { state: 'hidden', timeout: 10000 });
});
test('Verify draft for proxy settings are used in HTTP requests', async ({ page, createTmpDir }) => {
const collectionName = 'test-draft-proxy-settings';
// Create a new collection
await createCollection(page, collectionName, await createTmpDir());
await openCollectionAndAcceptSandbox(page, collectionName);
// Create a new request from collection menu
const collection = page.locator('.collection-name').filter({ hasText: collectionName });
await collection.locator('.collection-actions').hover();
await collection.locator('.collection-actions').click();
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
await page.getByTestId('request-name').fill('Test Request');
await page.getByTestId('new-request-url').locator('.CodeMirror').click();
await page.keyboard.type('https://testbench-sanity.usebruno.com/ping');
await page.getByRole('button', { name: 'Create', exact: true }).click();
// Verify the request is created
await expect(page.locator('.collection-item-name').filter({ hasText: 'Test Request' })).toBeVisible();
const request = page.locator('.collection-item-name').filter({ hasText: 'Test Request' });
// Run the request with inherit timeout
await page.getByTestId('send-arrow-icon').click();
await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 });
// Click on collection in sidebar to open collection settings
await page.locator('#sidebar-collection-name').filter({ hasText: collectionName }).click();
// Go to Proxy Settings tab
await page.locator('.tab.proxy').click();
await page.locator('input[name="enabled"][value="true"]').check();
await page.locator('#hostname').fill('localhost');
await page.locator('#port').fill('8080');
await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click();
// Run the request again
await page.getByTestId('send-arrow-icon').click();
await expect(page.getByText('Error occurred while executing the request!')).toBeVisible();
});
});

View File

@@ -0,0 +1,77 @@
syntax = "proto3";
package grpcbin;
service GRPCBin {
// This endpoint
rpc Index(EmptyMessage) returns (IndexReply) {}
// Unary endpoint that takes no argument and replies an empty message.
rpc Empty(EmptyMessage) returns (EmptyMessage) {}
// Unary endpoint that replies a received DummyMessage
rpc DummyUnary(DummyMessage) returns (DummyMessage) {}
// Stream endpoint that sends back 10 times the received DummyMessage
rpc DummyServerStream(DummyMessage) returns (stream DummyMessage) {}
// Stream endpoint that receives 10 DummyMessages and replies with the last received one
rpc DummyClientStream(stream DummyMessage) returns (DummyMessage) {}
// Stream endpoint that sends back a received DummyMessage indefinitely (chat mode)
rpc DummyBidirectionalStreamStream(stream DummyMessage) returns (stream DummyMessage) {}
// Unary endpoint that raises a specified (by code) gRPC error
rpc SpecificError(SpecificErrorRequest) returns (EmptyMessage) {}
// Unary endpoint that raises a random gRPC error
rpc RandomError(EmptyMessage) returns (EmptyMessage) {}
// Unary endpoint that returns headers
rpc HeadersUnary(EmptyMessage) returns (HeadersMessage) {}
// Unary endpoint that returns no respnose
rpc NoResponseUnary(EmptyMessage) returns (EmptyMessage) {}
}
message HeadersMessage {
message Values {
repeated string values = 1;
}
map<string, Values> Metadata = 1;
}
message SpecificErrorRequest {
uint32 code = 1;
string reason = 2;
}
message EmptyMessage {}
message DummyMessage {
message Sub {
string f_string = 1;
}
enum Enum {
ENUM_0 = 0;
ENUM_1 = 1;
ENUM_2 = 2;
}
string f_string = 1;
repeated string f_strings = 2;
int32 f_int32 = 3;
repeated int32 f_int32s = 4;
Enum f_enum = 5;
repeated Enum f_enums = 6;
Sub f_sub = 7;
repeated Sub f_subs = 8;
bool f_bool = 9;
repeated bool f_bools = 10;
int64 f_int64 = 11;
repeated int64 f_int64s= 12;
bytes f_bytes = 13;
repeated bytes f_bytess = 14;
float f_float = 15;
repeated float f_floats = 16;
// TODO: timestamp, duration, oneof, any, maps, fieldmask, wrapper type, struct, listvalue, value, nullvalue, deprecated
}
message IndexReply {
message Endpoint {
string path = 1;
string description = 2;
}
string description = 1;
repeated Endpoint endpoints = 2;
}

View File

@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDNTCCAh2gAwIBAgIUJr/uwo5anPA3YCk1swJPk4z0ZOswDQYJKoZIhvcNAQEL
BQAwKDESMBAGA1UEAwwJbWl0bXByb3h5MRIwEAYDVQQKDAltaXRtcHJveHkwHhcN
MjUwNjEwMTgwNTM3WhcNMzUwNjEwMTgwNTM3WjAoMRIwEAYDVQQDDAltaXRtcHJv
eHkxEjAQBgNVBAoMCW1pdG1wcm94eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAL+bc9tuU/bv7+20IhQ7lMeemBsZiiccWk11fUwYlO1oOgTh0YCQk04J
Wu7WvA52OTZp9CtwRCF+iUwHw8iIYlNMc9RBiTdfYA8KBxia3NEJBllPGxawGjzJ
CyjemGuC5f2pjRa2lVZnFBIfdEzYT9WyjsMovJAhhm88P17JF6jr2UTG5S8gzdyO
/ArKxDtNnebXOFKtxgiB1QAE3fm8EQC5neD6bUr+UfvHEAzIUhJfco5ckEk50yXR
heRNMnSOycQcMRwlO7/IGtTru+sM+tnrlXdMmX0j0dRzuZEDItGA78O/mMSdGgJJ
KwRf9MplHPx+F+7Bl30oz1I/QiDzqPkCAwEAAaNXMFUwDwYDVR0TAQH/BAUwAwEB
/zATBgNVHSUEDDAKBggrBgEFBQcDATAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
FEwsfBQerk5DimWXnYFPvrczmQL1MA0GCSqGSIb3DQEBCwUAA4IBAQCb+S04pxue
u9xtyGZJ32pxE9erUAB/ONYKSw0+ab2qdySBhNjRalwrm9NHlJoL/0g4p0pCV5sd
3lro5POrsfBcANdDSQ/e//jJG3gt/6ipgSVgeFW9LGx0INJAByhvkKvNbpWKiS9i
4iGGIFxzPQLac2lHL6BTgV0mwkHC1YI9zSLpunqiQFRbU497MbZDmLEw57i2C0MB
Rt7Ri9Ah0ajApPCofGFXvnKPf6SL4a0xkd3SUgXtovIdzTYPuhwXlJDoUkQuUs1G
hq0M++IKXL6DqFp+T+zDrnEWLuzJ0uLo1VDEMBIFbaK2WOh1sMaGz75No6s+kZlG
LkFX7Z0xl4iK
-----END CERTIFICATE-----

View File

@@ -1,10 +1,22 @@
import { execSync } from 'child_process';
import path from 'path';
import { test, expect } from '../../playwright';
import { closeAllCollections } from '../utils/page';
import path from 'path';
import fs from 'fs';
const COLLECTION_PATH = path.join(__dirname, 'collection', 'bruno.json');
const BACKUP_PATH = path.join(__dirname, 'collection', 'bruno.json.backup');
import { execSync } from 'child_process';
test.describe('manage protofile', () => {
test.beforeAll(async () => {
// Backup original file
if (fs.existsSync(COLLECTION_PATH)) {
fs.copyFileSync(COLLECTION_PATH, BACKUP_PATH);
}
});
test.afterAll(async ({ pageWithUserData: page }) => {
// Close all collections
await closeAllCollections(page);
// Reset the collection request file to the original state
execSync(`git checkout -- ${path.join(__dirname, 'collection', 'bruno.json')}`);
@@ -64,6 +76,9 @@ test.describe('manage protofile', () => {
await expect(page.getByRole('cell', { name: '../protos/invalid-import-path', exact: true })).not.toBeVisible();
await expect(invalidImportPathsMessage).not.toBeVisible();
// Save the changes to persist them to bruno.json
await page.getByRole('button', { name: 'Save' }).click();
});
test('order.proto loads methods successfully when selected', async ({ pageWithUserData: page }) => {
@@ -132,6 +147,9 @@ test.describe('manage protofile', () => {
const checkbox = page.getByRole('row', { name: 'Enable this import path types' }).getByTestId('protobuf-import-path-checkbox');
await checkbox.click();
// Save the changes to persist them to bruno.json
await page.getByRole('button', { name: 'Save' }).click();
// Now test that product.proto can load methods successfully
await page.getByText('HelloService').click();
await page.getByText('SayHello').click();