diff --git a/packages/bruno-app/src/components/FolderSettings/Headers/StyledWrapper.js b/packages/bruno-app/src/components/FolderSettings/Headers/StyledWrapper.js
new file mode 100644
index 000000000..9f723cb81
--- /dev/null
+++ b/packages/bruno-app/src/components/FolderSettings/Headers/StyledWrapper.js
@@ -0,0 +1,56 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.div`
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ font-weight: 600;
+ table-layout: fixed;
+
+ thead,
+ td {
+ border: 1px solid ${(props) => props.theme.table.border};
+ }
+
+ thead {
+ color: ${(props) => props.theme.table.thead.color};
+ font-size: 0.8125rem;
+ user-select: none;
+ }
+ td {
+ padding: 6px 10px;
+
+ &:nth-child(1) {
+ width: 30%;
+ }
+
+ &:nth-child(3) {
+ width: 70px;
+ }
+ }
+ }
+
+ .btn-add-header {
+ font-size: 0.8125rem;
+ }
+
+ input[type='text'] {
+ width: 100%;
+ border: solid 1px transparent;
+ outline: none !important;
+ background-color: inherit;
+
+ &:focus {
+ outline: none !important;
+ border: solid 1px transparent;
+ }
+ }
+
+ input[type='checkbox'] {
+ cursor: pointer;
+ position: relative;
+ top: 1px;
+ }
+`;
+
+export default Wrapper;
diff --git a/packages/bruno-app/src/components/FolderSettings/Headers/index.js b/packages/bruno-app/src/components/FolderSettings/Headers/index.js
new file mode 100644
index 000000000..a944285e6
--- /dev/null
+++ b/packages/bruno-app/src/components/FolderSettings/Headers/index.js
@@ -0,0 +1,150 @@
+import React from 'react';
+import get from 'lodash/get';
+import cloneDeep from 'lodash/cloneDeep';
+import { IconTrash } from '@tabler/icons';
+import { useDispatch } from 'react-redux';
+import { useTheme } from 'providers/Theme';
+import { addFolderHeader, updateFolderHeader, deleteFolderHeader } from 'providers/ReduxStore/slices/collections';
+import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
+import SingleLineEditor from 'components/SingleLineEditor';
+import StyledWrapper from './StyledWrapper';
+import { headers as StandardHTTPHeaders } from 'know-your-http-well';
+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 addHeader = () => {
+ dispatch(
+ addFolderHeader({
+ collectionUid: collection.uid,
+ folderUid: folder.uid
+ })
+ );
+ };
+
+ const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
+ const handleHeaderValueChange = (e, _header, type) => {
+ const header = cloneDeep(_header);
+ switch (type) {
+ case 'name': {
+ header.name = e.target.value;
+ break;
+ }
+ case 'value': {
+ header.value = e.target.value;
+ break;
+ }
+ case 'enabled': {
+ header.enabled = e.target.checked;
+ break;
+ }
+ }
+ dispatch(
+ updateFolderHeader({
+ header: header,
+ collectionUid: collection.uid,
+ folderUid: folder.uid
+ })
+ );
+ };
+
+ const handleRemoveHeader = (header) => {
+ dispatch(
+ deleteFolderHeader({
+ headerUid: header.uid,
+ collectionUid: collection.uid,
+ folderUid: folder.uid
+ })
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+export default Headers;
diff --git a/packages/bruno-app/src/components/FolderSettings/index.js b/packages/bruno-app/src/components/FolderSettings/index.js
new file mode 100644
index 000000000..b1d6ee249
--- /dev/null
+++ b/packages/bruno-app/src/components/FolderSettings/index.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import classnames from 'classnames';
+import { updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
+import { useDispatch } from 'react-redux';
+import Headers from './Headers';
+
+const FolderSettings = ({ collection, folder }) => {
+ const dispatch = useDispatch();
+ const tab = folder?.settingsSelectedTab || 'headers';
+ const setTab = (tab) => {
+ dispatch(
+ updateSettingsSelectedTab({
+ collectionUid: folder.collectionUid,
+ folderUid: folder.uid,
+ tab
+ })
+ );
+ };
+
+ const getTabPanel = (tab) => {
+ switch (tab) {
+ case 'headers': {
+ return ;
+ }
+ // TODO: Add auth
+ }
+ };
+
+ const getTabClassname = (tabName) => {
+ return classnames(`tab select-none ${tabName}`, {
+ active: tabName === tab
+ });
+ };
+
+ return (
+
+
+
setTab('headers')}>
+ Headers
+
+ {/*
setTab('auth')}>
+ Auth
+
*/}
+
+
+
+ );
+};
+
+export default FolderSettings;
diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js
index f719eb0f3..2fd253f4b 100644
--- a/packages/bruno-app/src/components/RequestTabPanel/index.js
+++ b/packages/bruno-app/src/components/RequestTabPanel/index.js
@@ -18,6 +18,7 @@ import CollectionSettings from 'components/CollectionSettings';
import { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper';
+import FolderSettings from 'components/FolderSettings';
const MIN_LEFT_PANE_WIDTH = 300;
const MIN_RIGHT_PANE_WIDTH = 350;
@@ -131,6 +132,10 @@ const RequestTabPanel = () => {
if (focusedTab.type === 'collection-settings') {
return ;
}
+ if (focusedTab.type === 'folder-settings') {
+ const folder = findItemInCollection(collection, focusedTab.folderUid);
+ return ;
+ }
const item = findItemInCollection(collection, activeTabUid);
if (!item || !item.uid) {
diff --git a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js
index ba77d47c9..dcfacb260 100644
--- a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js
+++ b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js
@@ -44,7 +44,7 @@ const CollectionToolBar = ({ collection }) => {
- {collection.name}
+ {collection?.name || 'Folder'}
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js
index aebc3db75..7510af30b 100644
--- a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js
+++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js
@@ -1,22 +1,30 @@
import React from 'react';
import { IconVariable, IconSettings, IconRun } from '@tabler/icons';
-const SpecialTab = ({ handleCloseClick, type }) => {
- const getTabInfo = (type) => {
+const SpecialTab = ({ handleCloseClick, type, folderName }) => {
+ const getTabInfo = (type, folderName) => {
switch (type) {
case 'collection-settings': {
return (
<>
- Collection
+ Collection
>
);
}
+ case 'folder-settings': {
+ return (
+
+
+ {folderName || 'Folder'}
+
+ );
+ }
case 'variables': {
return (
<>
- Variables
+ Variables
>
);
}
@@ -24,7 +32,7 @@ const SpecialTab = ({ handleCloseClick, type }) => {
return (
<>
- Runner
+ Runner
>
);
}
@@ -33,7 +41,7 @@ const SpecialTab = ({ handleCloseClick, type }) => {
return (
<>
- {getTabInfo(type)}
+ {getTabInfo(type, folderName)}
handleCloseClick(e)}>
+ {isFolder && (
+ {
+ dropdownTippyRef.current.hide();
+ viewFolderSettings();
+ }}
+ >
+ Settings
+
+ )}
diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js
index 467a8582c..133185d9a 100644
--- a/packages/bruno-app/src/providers/App/useIpcEvents.js
+++ b/packages/bruno-app/src/providers/App/useIpcEvents.js
@@ -11,6 +11,7 @@ import {
collectionUnlinkFileEvent,
processEnvUpdateEvent,
runFolderEvent,
+ folderAddFileEvent,
runRequestEvent,
scriptEnvironmentUpdateEvent
} from 'providers/ReduxStore/slices/collections';
@@ -48,6 +49,13 @@ const useIpcEvents = () => {
})
);
}
+ if (type === 'addFileDir') {
+ dispatch(
+ folderAddFileEvent({
+ file: val
+ })
+ );
+ }
if (type === 'change') {
dispatch(
collectionChangeFileEvent({
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 526b43a1e..3c3d1786e 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -14,10 +14,11 @@ import {
findParentItemInCollection,
getItemsToResequence,
isItemAFolder,
+ refreshUidsInItem,
+ findItemInCollectionByPathname,
isItemARequest,
moveCollectionItem,
moveCollectionItemToRootOfCollection,
- refreshUidsInItem,
transformRequestToSaveToFilesystem
} from 'utils/collections';
import { uuid, waitForNextTick } from 'utils/common';
@@ -143,7 +144,65 @@ export const saveCollectionRoot = (collectionUid) => (dispatch, getState) => {
});
};
-export const sendCollectionOauth2Request = (collectionUid) => (dispatch, getState) => {
+export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState) => {
+ const state = getState();
+ const collection = findCollectionByUid(state.collections.collections, collectionUid);
+ const folder = findItemInCollection(collection, folderUid);
+
+ return new Promise((resolve, reject) => {
+ if (!collection) {
+ return reject(new Error('Collection not found'));
+ }
+
+ if (!folder) {
+ return reject(new Error('Folder not found'));
+ }
+
+ const { ipcRenderer } = window;
+
+ ipcRenderer
+ .invoke('renderer:save-folder-root', folder.pathname, folder.root)
+ .then(() => toast.success('Folder Settings saved successfully'))
+ .then(resolve)
+ .catch((err) => {
+ toast.error('Failed to save folder settings!');
+ reject(err);
+ });
+ });
+};
+
+export const retrieveDirectoriesBetween = (pathname, parameter, filename) => {
+ const parameterIndex = pathname.indexOf(parameter);
+ const filenameIndex = pathname.indexOf(filename);
+ if (parameterIndex === -1 || filenameIndex === -1 || filenameIndex < parameterIndex) {
+ return [];
+ }
+ const directories = pathname
+ .substring(parameterIndex + parameter.length, filenameIndex)
+ .split('/')
+ .filter((directory) => directory.trim() !== '');
+ const reconstructedPaths = [];
+ let currentPath = pathname.substring(0, parameterIndex + parameter.length);
+ for (const directory of directories) {
+ currentPath += `/${directory}`;
+ reconstructedPaths.push(currentPath);
+ }
+ return reconstructedPaths;
+};
+
+export const mergeRequests = (parentRequest, childRequest) => {
+ return _.mergeWith({}, parentRequest, childRequest, customizer);
+};
+
+function customizer(objValue, srcValue, key) {
+ const exceptions = ['headers', 'params', 'vars'];
+ if (exceptions.includes(key) && _.isArray(objValue) && _.isArray(srcValue)) {
+ return _.unionBy(srcValue, objValue, 'name');
+ }
+ return undefined;
+}
+
+export const sendCollectionOauth2Request = (collectionUid, itemUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -156,7 +215,10 @@ export const sendCollectionOauth2Request = (collectionUid) => (dispatch, getStat
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
- _sendCollectionOauth2Request(collection, environment, collectionCopy.collectionVariables)
+ const externalSecrets = getExternalCollectionSecretsForActiveEnvironment({ collection });
+ const secretVariables = getFormattedCollectionSecretVariables({ externalSecrets });
+
+ _sendCollectionOauth2Request(collection, environment, collectionCopy.collectionVariables, itemUid, secretVariables)
.then((response) => {
if (response?.data?.error) {
toast.error(response?.data?.error);
@@ -184,9 +246,26 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
const itemCopy = cloneDeep(item || {});
const collectionCopy = cloneDeep(collection);
- const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
+ const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);
+ const itemTree = retrieveDirectoriesBetween(itemCopy.pathname, collectionCopy.name, itemCopy.filename);
- sendNetworkRequest(itemCopy, collection, environment, collectionCopy.collectionVariables)
+ const folderDatas = itemTree.reduce((acc, currentPath) => {
+ const folder = findItemInCollectionByPathname(collectionCopy, currentPath);
+ if (folder) {
+ acc = mergeRequests(acc, folder.root.request);
+ }
+ return acc;
+ }, {});
+ const mergeParams = mergeRequests(collectionCopy.root.request, folderDatas);
+ // merge collection and folder settings with request
+ const mergedCollection = {
+ ...collectionCopy,
+ root: {
+ ...collectionCopy.root,
+ request: mergeParams
+ }
+ };
+ sendNetworkRequest(itemCopy, mergedCollection, environment, collectionCopy.collectionVariables)
.then((response) => {
return dispatch(
responseReceived({
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 2a851c238..64cf4f654 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -89,7 +89,7 @@ export const collectionsSlice = createSlice({
}
},
updateSettingsSelectedTab: (state, action) => {
- const { collectionUid, tab } = action.payload;
+ const { collectionUid, folderUid, tab } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -1114,6 +1114,44 @@ export const collectionsSlice = createSlice({
set(collection, 'root.docs', action.payload.docs);
}
},
+ 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', []);
+ headers.push({
+ uid: uuid(),
+ name: '',
+ value: '',
+ description: '',
+ enabled: true
+ });
+ set(folder, 'root.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', []);
+ const header = find(headers, (h) => h.uid === action.payload.header.uid);
+ if (header) {
+ header.name = action.payload.header.name;
+ header.value = action.payload.header.value;
+ header.description = action.payload.header.description;
+ header.enabled = action.payload.header.enabled;
+ }
+ }
+ },
+ deleteFolderHeader: (state, action) => {
+ 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', []);
+ headers = filter(headers, (h) => h.uid !== action.payload.headerUid);
+ set(folder, 'root.request.headers', headers);
+ }
+ },
addCollectionHeader: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@@ -1152,11 +1190,22 @@ export const collectionsSlice = createSlice({
set(collection, 'root.request.headers', headers);
}
},
+ folderAddFileEvent: (state, action) => {
+ const file = action.payload.file;
+ const isFolderRoot = file.meta.folderRoot ? true : false;
+ const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
+ const folder = findItemInCollectionByPathname(collection, file.meta.pathname);
+ if (isFolderRoot) {
+ if (folder) {
+ folder.root = file.data;
+ }
+ return;
+ }
+ },
collectionAddFileEvent: (state, action) => {
const file = action.payload.file;
const isCollectionRoot = file.meta.collectionRoot ? true : false;
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
-
if (isCollectionRoot) {
if (collection) {
collection.root = file.data;
@@ -1187,7 +1236,7 @@ export const collectionsSlice = createSlice({
currentSubItems = childItem.items;
}
- if (!currentSubItems.find((f) => f.name === file.meta.name)) {
+ if (file.meta.name != 'folder.bru' && !currentSubItems.find((f) => f.name === file.meta.name)) {
// this happens when you rename a file
// the add event might get triggered first, before the unlink event
// this results in duplicate uids causing react renderer to go mad
@@ -1521,6 +1570,9 @@ export const {
addVar,
updateVar,
deleteVar,
+ addFolderHeader,
+ updateFolderHeader,
+ deleteFolderHeader,
addCollectionHeader,
updateCollectionHeader,
deleteCollectionHeader,
@@ -1537,6 +1589,7 @@ export const {
collectionUnlinkDirectoryEvent,
collectionAddEnvFileEvent,
collectionRenamedEvent,
+ folderAddFileEvent,
resetRunResults,
runRequestEvent,
runFolderEvent,
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js
index 74c503dad..b64a71fad 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js
@@ -38,7 +38,8 @@ export const tabsSlice = createSlice({
requestPaneWidth: null,
requestPaneTab: action.payload.requestPaneTab || 'params',
responsePaneTab: 'response',
- type: action.payload.type || 'request'
+ type: action.payload.type || 'request',
+ ...(action.payload.folderUid ? { folderUid: action.payload.folderUid } : {})
});
state.activeTabUid = action.payload.uid;
},
diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js
index 35192b128..e51daa552 100644
--- a/packages/bruno-cli/src/commands/run.js
+++ b/packages/bruno-cli/src/commands/run.js
@@ -179,6 +179,17 @@ const getCollectionRoot = (dir) => {
return collectionBruToJson(content);
};
+const getFolderRoot = (dir) => {
+ const folderRootPath = path.join(dir, 'folder.bru');
+ const exists = fs.existsSync(folderRootPath);
+ if (!exists) {
+ return {};
+ }
+
+ const content = fs.readFileSync(folderRootPath, 'utf8');
+ return collectionBruToJson(content);
+};
+
const builder = async (yargs) => {
yargs
.option('r', {
diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js
index 441bba3b2..6b7c6c31b 100644
--- a/packages/bruno-electron/src/app/watcher.js
+++ b/packages/bruno-electron/src/app/watcher.js
@@ -40,10 +40,42 @@ const isBruEnvironmentConfig = (pathname, collectionPath) => {
const isCollectionRootBruFile = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
const basename = path.basename(pathname);
-
return dirname === collectionPath && basename === 'collection.bru';
};
+const isFolderRootBruFile = (pathname, folderPath) => {
+ const dirname = path.dirname(pathname);
+ const basename = path.basename(pathname);
+ return dirname === folderPath && basename === 'folder.bru';
+};
+
+const scanDirectory = (directoryPath, callback) => {
+ fs.readdir(directoryPath, (err, files) => {
+ if (err) {
+ console.error(`Error reading directory ${directoryPath}: ${err}`);
+ return;
+ }
+ if (files.includes('folder.bru')) {
+ callback(directoryPath);
+ }
+ // Iterate through each file/folder in the directory
+ files.forEach((file) => {
+ const filePath = path.join(directoryPath, file);
+ // Check if it's a directory
+ fs.stat(filePath, (err, stats) => {
+ if (err) {
+ console.error(`Error statting ${filePath}: ${err}`);
+ return;
+ }
+ // If it's a directory, recursively scan it
+ if (stats.isDirectory()) {
+ scanDirectory(filePath, callback);
+ }
+ });
+ });
+ });
+};
+
const hydrateRequestWithUuid = (request, pathname) => {
request.uid = getRequestUid(pathname);
@@ -225,12 +257,27 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
collectionRoot: true
}
};
-
+ const folderCallback = (filePath) => {
+ const bruContent = fs.readFileSync(`${filePath}/folder.bru`, 'utf8');
+ if (bruContent) {
+ const folder = {
+ meta: {
+ collectionUid,
+ pathname: filePath,
+ name: path.basename(filePath),
+ folderRoot: true
+ }
+ };
+ folder.data = collectionBruToJson(bruContent);
+ hydrateBruCollectionFileWithUuid(folder.data);
+ win.webContents.send('main:collection-tree-updated', 'addFileDir', folder);
+ }
+ };
try {
+ scanDirectory(collectionPath, folderCallback);
let bruContent = fs.readFileSync(pathname, 'utf8');
file.data = collectionBruToJson(bruContent);
-
hydrateBruCollectionFileWithUuid(file.data);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
return;
@@ -334,7 +381,6 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
let bruContent = fs.readFileSync(pathname, 'utf8');
file.data = collectionBruToJson(bruContent);
-
hydrateBruCollectionFileWithUuid(file.data);
win.webContents.send('main:collection-tree-updated', 'change', file);
return;
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index 5f8b63c3b..c7901edb2 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -152,6 +152,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
+ ipcMain.handle('renderer:save-folder-root', async (event, folderPathname, folderRoot) => {
+ try {
+ const folderBruFilePath = path.join(folderPathname, 'folder.bru');
+
+ const content = jsonToBru(folderRoot);
+ await writeFile(folderBruFilePath, content);
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
ipcMain.handle('renderer:save-collection-root', async (event, collectionPathname, collectionRoot) => {
try {
const collectionBruFilePath = path.join(collectionPathname, 'collection.bru');