diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/ApiKeyAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/ApiKeyAuth/index.js index 74d348c64..2de1495c8 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/ApiKeyAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/ApiKeyAuth/index.js @@ -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 ( diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js index c7f157a43..f7bd8f0b1 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js @@ -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 ( diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js index 12e35cfea..6d7682414 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js @@ -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( diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js index d0cf9d722..4464ef2ee 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js @@ -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( diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/BearerAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/BearerAuth/index.js index 788182479..2806f1df0 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/BearerAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/BearerAuth/index.js @@ -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( diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js index 22981f56b..ed85ca436 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js @@ -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( diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/index.js index 38a9c18f0..50e1bbbc6 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/index.js @@ -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) => { diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js index e8d20f25c..2dfe54c44 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js @@ -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 ( diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js index 226cedd7b..a90923e74 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js @@ -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( diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js index c19ae9873..af457e6f4 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js @@ -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) { diff --git a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js index a9d7255ea..083380c58 100644 --- a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js @@ -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 (
Add client certificates to be used for specific domains.
@@ -118,9 +161,9 @@ const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove } {clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath} - + ))} @@ -329,10 +372,14 @@ const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }
{formik.errors.passphrase}
) : null} -
+
+
+
diff --git a/packages/bruno-app/src/components/CollectionSettings/Docs/index.js b/packages/bruno-app/src/components/CollectionSettings/Docs/index.js index 3289e099c..38d314de5 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Docs/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Docs/index.js @@ -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(); } diff --git a/packages/bruno-app/src/components/CollectionSettings/Headers/index.js b/packages/bruno-app/src/components/CollectionSettings/Headers/index.js index d0968c425..45e3e5834 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Headers/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Headers/index.js @@ -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) { diff --git a/packages/bruno-app/src/components/CollectionSettings/Presets/index.js b/packages/bruno-app/src/components/CollectionSettings/Presets/index.js index 4f1168180..30fd44081 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Presets/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Presets/index.js @@ -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 (
These presets will be used as the default values for new requests in this collection.
-
+
@@ -102,11 +109,11 @@ const PresetsSettings = ({ collection }) => {
-
-
+
); }; diff --git a/packages/bruno-app/src/components/CollectionSettings/Protobuf/index.js b/packages/bruno-app/src/components/CollectionSettings/Protobuf/index.js index 845bea6c6..10385f6e7 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Protobuf/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Protobuf/index.js @@ -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 }) => {
- + {getBasename(collection.pathname, file.path)} {!isValid && } @@ -329,6 +334,12 @@ const ProtobufSettings = ({ collection }) => {
+
+ +
+
); }; diff --git a/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js b/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js index bb48cbdc0..039b2eab8 100644 --- a/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js @@ -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 (
Configure proxy settings for this collection.
-
+
@@ -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 ? ( -
{formik.errors.auth.username}
- ) : null}
- {formik.touched.auth?.password && formik.errors.auth?.password ? ( -
{formik.errors.auth.password}
- ) : null}
@@ -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 ? ( -
{formik.errors.bypassProxy}
- ) : null}
-
-
+
); }; diff --git a/packages/bruno-app/src/components/CollectionSettings/Script/index.js b/packages/bruno-app/src/components/CollectionSettings/Script/index.js index 625df1ff7..99e4c638d 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Script/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Script/index.js @@ -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 ( diff --git a/packages/bruno-app/src/components/CollectionSettings/Tests/index.js b/packages/bruno-app/src/components/CollectionSettings/Tests/index.js index 975758ee1..cd7162801 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Tests/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Tests/index.js @@ -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 ( diff --git a/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js b/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js index 0341c6ecd..fd15eee8c 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js @@ -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) { diff --git a/packages/bruno-app/src/components/CollectionSettings/Vars/index.js b/packages/bruno-app/src/components/CollectionSettings/Vars/index.js index fae3ed613..02375f19a 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Vars/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Vars/index.js @@ -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 (
diff --git a/packages/bruno-app/src/components/CollectionSettings/index.js b/packages/bruno-app/src/components/CollectionSettings/index.js index beb4f040e..7d053a0c8 100644 --- a/packages/bruno-app/src/components/CollectionSettings/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/index.js @@ -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 ; } case 'proxy': { - return ; + return ; } case 'clientCert': { return ( ); } diff --git a/packages/bruno-app/src/components/FolderSettings/Auth/index.js b/packages/bruno-app/src/components/FolderSettings/Auth/index.js index 0bb8a1c37..0e14c8c03 100644 --- a/packages/bruno-app/src/components/FolderSettings/Auth/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Auth/index.js @@ -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', diff --git a/packages/bruno-app/src/components/FolderSettings/AuthMode/index.js b/packages/bruno-app/src/components/FolderSettings/AuthMode/index.js index 36377973a..160693c96 100644 --- a/packages/bruno-app/src/components/FolderSettings/AuthMode/index.js +++ b/packages/bruno-app/src/components/FolderSettings/AuthMode/index.js @@ -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 ( diff --git a/packages/bruno-app/src/components/FolderSettings/Documentation/index.js b/packages/bruno-app/src/components/FolderSettings/Documentation/index.js index 964afdece..35decfcfe 100644 --- a/packages/bruno-app/src/components/FolderSettings/Documentation/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Documentation/index.js @@ -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); diff --git a/packages/bruno-app/src/components/FolderSettings/Headers/index.js b/packages/bruno-app/src/components/FolderSettings/Headers/index.js index 79f22a0b8..82730c7fc 100644 --- a/packages/bruno-app/src/components/FolderSettings/Headers/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Headers/index.js @@ -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 = () => { diff --git a/packages/bruno-app/src/components/FolderSettings/Script/index.js b/packages/bruno-app/src/components/FolderSettings/Script/index.js index 5c3ca5b0d..70631cc57 100644 --- a/packages/bruno-app/src/components/FolderSettings/Script/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Script/index.js @@ -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); diff --git a/packages/bruno-app/src/components/FolderSettings/Tests/index.js b/packages/bruno-app/src/components/FolderSettings/Tests/index.js index ae20a3b8e..371883940 100644 --- a/packages/bruno-app/src/components/FolderSettings/Tests/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Tests/index.js @@ -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); diff --git a/packages/bruno-app/src/components/FolderSettings/Vars/index.js b/packages/bruno-app/src/components/FolderSettings/Vars/index.js index 8f9cab4d2..69f901fab 100644 --- a/packages/bruno-app/src/components/FolderSettings/Vars/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Vars/index.js @@ -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 ( diff --git a/packages/bruno-app/src/components/FolderSettings/index.js b/packages/bruno-app/src/components/FolderSettings/index.js index 61170e5ca..c422815e0 100644 --- a/packages/bruno-app/src/components/FolderSettings/index.js +++ b/packages/bruno-app/src/components/FolderSettings/index.js @@ -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; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/index.js index 7725f07d1..c1043918c 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/index.js @@ -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', diff --git a/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/ProtoFileDropdown/index.js b/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/ProtoFileDropdown/index.js index a51aef472..6d23f042d 100644 --- a/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/ProtoFileDropdown/index.js +++ b/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/ProtoFileDropdown/index.js @@ -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}`); diff --git a/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/index.js b/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/index.js index 9f9ea9070..ee1f81c06 100644 --- a/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/index.js @@ -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', diff --git a/packages/bruno-app/src/components/RequestPane/WSRequestPane/WSAuth/index.js b/packages/bruno-app/src/components/RequestPane/WSRequestPane/WSAuth/index.js index 59d1bae32..ea8fa08b2 100644 --- a/packages/bruno-app/src/components/RequestPane/WSRequestPane/WSAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/WSRequestPane/WSAuth/index.js @@ -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', diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/ConfirmCollectionClose/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/ConfirmCollectionClose/index.js new file mode 100644 index 000000000..729e10c52 --- /dev/null +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/ConfirmCollectionClose/index.js @@ -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 ( + { + e.stopPropagation(); + e.preventDefault(); + }} + hideFooter={true} + > +
+ +

Hold on..

+
+
+ You have unsaved changes in {collection.name} collection settings. +
+ +
+
+ +
+
+ + +
+
+
+ ); +}; + +export default ConfirmCollectionClose; diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/ConfirmFolderClose/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/ConfirmFolderClose/index.js new file mode 100644 index 000000000..42e4cad23 --- /dev/null +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/ConfirmFolderClose/index.js @@ -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 ( + { + e.stopPropagation(); + e.preventDefault(); + }} + hideFooter={true} + > +
+ +

Hold on..

+
+
+ You have unsaved changes in {folder.name} folder settings. +
+ +
+
+ +
+
+ + +
+
+
+ ); +}; + +export default ConfirmFolderClose; diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js index b895c10fe..12e919802 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js @@ -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 }) => { <>
{getTabInfo(type, tabName)}
handleCloseClick(e)}> - + {hasDraft ? : }
); diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index 30ceaa018..446cb2b98 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -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 ( + {showConfirmCollectionClose && tab.type === 'collection-settings' && ( + 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' && ( + 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 ? ( ) : tab.type === 'folder-settings' ? ( - dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} /> + dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} hasDraft={hasFolderDraft} /> + ) : tab.type === 'collection-settings' ? ( + dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={collection?.name} hasDraft={hasDraft} /> ) : ( dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} /> )} diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/index.js index 70cae8332..693f26438 100644 --- a/packages/bruno-app/src/components/ResponsePane/Timeline/index.js +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/index.js @@ -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, diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/auth-utils.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/auth-utils.js index f9885f0df..45e396625 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/auth-utils.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/auth-utils.js @@ -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; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js index 41d9236ed..effc1c158 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js @@ -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]; } diff --git a/packages/bruno-app/src/hooks/useProtoFileManagement/index.js b/packages/bruno-app/src/hooks/useProtoFileManagement/index.js index 7740d3e9e..7c757c823 100644 --- a/packages/bruno-app/src/hooks/useProtoFileManagement/index.js +++ b/packages/bruno-app/src/hooks/useProtoFileManagement/index.js @@ -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 }; } diff --git a/packages/bruno-app/src/providers/App/ConfirmAppClose/SaveRequestsModal.js b/packages/bruno-app/src/providers/App/ConfirmAppClose/SaveRequestsModal.js index 342f60b52..fe532e006 100644 --- a/packages/bruno-app/src/providers/App/ConfirmAppClose/SaveRequestsModal.js +++ b/packages/bruno-app/src/providers/App/ConfirmAppClose/SaveRequestsModal.js @@ -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 }) => {

Do you want to save the changes you made to the following{' '} - {currentDrafts.length} {pluralizeWord('request', currentDrafts.length)}? + {totalDraftsCount} {pluralizeWord('item', totalDraftsCount)}?

- {currentDrafts.length > MAX_UNSAVED_REQUESTS_TO_SHOW && ( + {totalDraftsCount > MAX_UNSAVED_ITEMS_TO_SHOW && (

- ...{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

)} @@ -108,7 +166,7 @@ const SaveRequestsModal = ({ onClose }) => { Cancel diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js index 0073d73bf..48f1b6e90 100644 --- a/packages/bruno-app/src/providers/Hotkeys/index.js +++ b/packages/bruno-app/src/providers/Hotkeys/index.js @@ -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)); } } } diff --git a/packages/bruno-app/src/providers/ReduxStore/middlewares/draft/middleware.js b/packages/bruno-app/src/providers/ReduxStore/middlewares/draft/middleware.js index 4b8f39443..a21b21768 100644 --- a/packages/bruno-app/src/providers/ReduxStore/middlewares/draft/middleware.js +++ b/packages/bruno-app/src/providers/ReduxStore/middlewares/draft/middleware.js @@ -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) => { diff --git a/packages/bruno-app/src/providers/ReduxStore/middlewares/draft/utils.js b/packages/bruno-app/src/providers/ReduxStore/middlewares/draft/utils.js index ab84ccaf4..ede864893 100644 --- a/packages/bruno-app/src/providers/ReduxStore/middlewares/draft/utils.js +++ b/packages/bruno-app/src/providers/ReduxStore/middlewares/draft/utils.js @@ -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 { diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index c886dece9..9e58f15f8 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -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(); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index fde2fc435..e6c7ae880 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -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, diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index f84309478..160b05c81 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -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; diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index 89fc1dbd0..607676d36 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -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 = { diff --git a/packages/bruno-cli/src/utils/collection.js b/packages/bruno-cli/src/utils/collection.js index 2cf865a57..068cbff41 100644 --- a/packages/bruno-cli/src/utils/collection.js +++ b/packages/bruno-cli/src/utils/collection.js @@ -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; } diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 77db2bdd2..d38a02f03 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -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, diff --git a/packages/bruno-electron/src/ipc/network/cert-utils.js b/packages/bruno-electron/src/ipc/network/cert-utils.js index c15a89030..b55754b7b 100644 --- a/packages/bruno-electron/src/ipc/network/cert-utils.js +++ b/packages/bruno-electron/src/ipc/network/cert-utils.js @@ -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, diff --git a/packages/bruno-electron/src/ipc/network/grpc-event-handlers.js b/packages/bruno-electron/src/ipc/network/grpc-event-handlers.js index 0c96c1344..26ee77c47 100644 --- a/packages/bruno-electron/src/ipc/network/grpc-event-handlers.js +++ b/packages/bruno-electron/src/ipc/network/grpc-event-handlers.js @@ -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); diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 1fff7de2b..611182a27 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -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, diff --git a/packages/bruno-electron/src/ipc/network/prepare-grpc-request.js b/packages/bruno-electron/src/ipc/network/prepare-grpc-request.js index 657450dab..3471bea39 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-grpc-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-grpc-request.js @@ -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; diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 514c7b92e..5e37ac8ed 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -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; diff --git a/packages/bruno-electron/src/ipc/network/ws-event-handlers.js b/packages/bruno-electron/src/ipc/network/ws-event-handlers.js index fa7dd07f2..9513f9a6c 100644 --- a/packages/bruno-electron/src/ipc/network/ws-event-handlers.js +++ b/packages/bruno-electron/src/ipc/network/ws-event-handlers.js @@ -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 = {}; diff --git a/packages/bruno-electron/src/store/bruno-config.js b/packages/bruno-electron/src/store/bruno-config.js index 8942dd833..d4c5d83a9 100644 --- a/packages/bruno-electron/src/store/bruno-config.js +++ b/packages/bruno-electron/src/store/bruno-config.js @@ -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] || {}; }; diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index 1eef73fcb..d60e55bd5 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -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; diff --git a/packages/bruno-electron/tests/network/index.spec.js b/packages/bruno-electron/tests/network/index.spec.js index 02a9b9083..02cf97f88 100644 --- a/packages/bruno-electron/tests/network/index.spec.js +++ b/packages/bruno-electron/tests/network/index.spec.js @@ -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'); }); }); diff --git a/tests/collection/draft/draft-indicator.spec.ts b/tests/collection/draft/draft-indicator.spec.ts new file mode 100644 index 000000000..cffd08363 --- /dev/null +++ b/tests/collection/draft/draft-indicator.spec.ts @@ -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(); + }); +}); diff --git a/tests/collection/draft/draft-values-in-requests.spec.ts b/tests/collection/draft/draft-values-in-requests.spec.ts new file mode 100644 index 000000000..6cff4c811 --- /dev/null +++ b/tests/collection/draft/draft-values-in-requests.spec.ts @@ -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(); + }); +}); diff --git a/tests/collection/draft/fixtures/grpcbin.proto b/tests/collection/draft/fixtures/grpcbin.proto new file mode 100644 index 000000000..4729bee2c --- /dev/null +++ b/tests/collection/draft/fixtures/grpcbin.proto @@ -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 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; +} diff --git a/tests/collection/draft/fixtures/mitmproxy-ca-cert.cer b/tests/collection/draft/fixtures/mitmproxy-ca-cert.cer new file mode 100644 index 000000000..9f6b49521 --- /dev/null +++ b/tests/collection/draft/fixtures/mitmproxy-ca-cert.cer @@ -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----- diff --git a/tests/protobuf/manage-protofile.spec.ts b/tests/protobuf/manage-protofile.spec.ts index 787448d65..5d9895745 100644 --- a/tests/protobuf/manage-protofile.spec.ts +++ b/tests/protobuf/manage-protofile.spec.ts @@ -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();