mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
add: draft for collection and folder settings (#5947)
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
|
||||
@@ -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] || {};
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
360
tests/collection/draft/draft-indicator.spec.ts
Normal file
360
tests/collection/draft/draft-indicator.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
163
tests/collection/draft/draft-values-in-requests.spec.ts
Normal file
163
tests/collection/draft/draft-values-in-requests.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
77
tests/collection/draft/fixtures/grpcbin.proto
Normal file
77
tests/collection/draft/fixtures/grpcbin.proto
Normal 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;
|
||||
}
|
||||
20
tests/collection/draft/fixtures/mitmproxy-ca-cert.cer
Normal file
20
tests/collection/draft/fixtures/mitmproxy-ca-cert.cer
Normal 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-----
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user