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];
}