mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-21 23:43:15 +01:00
feat: folder level scripts and tests
This commit is contained in:
parent
c1a57d30dc
commit
ede29e29bd
79
package-lock.json
generated
79
package-lock.json
generated
@ -9996,32 +9996,6 @@
|
||||
"graphql": ">=0.11 <=16"
|
||||
}
|
||||
},
|
||||
"node_modules/handlebars": {
|
||||
"version": "4.7.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5",
|
||||
"neo-async": "^2.6.2",
|
||||
"source-map": "^0.6.1",
|
||||
"wordwrap": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"handlebars": "bin/handlebars"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.7"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"uglify-js": "^3.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/handlebars/node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/har-schema": {
|
||||
"version": "2.0.0",
|
||||
"license": "ISC",
|
||||
@ -10433,7 +10407,6 @@
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
@ -12985,6 +12958,7 @@
|
||||
},
|
||||
"node_modules/neo-async": {
|
||||
"version": "2.6.2",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/new-github-issue-url": {
|
||||
@ -16443,6 +16417,7 @@
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@ -17737,17 +17712,6 @@
|
||||
"version": "1.0.6",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uglify-js": {
|
||||
"version": "3.17.4",
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"uglifyjs": "bin/uglifyjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/underscore": {
|
||||
"version": "1.6.0"
|
||||
},
|
||||
@ -18363,10 +18327,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wordwrap": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"license": "MIT",
|
||||
@ -19753,6 +19713,7 @@
|
||||
"graphql": "^16.6.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"is-valid-path": "^0.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-bigint": "^1.0.0",
|
||||
@ -20806,6 +20767,7 @@
|
||||
"version": "0.12.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/query": "0.1.0",
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
@ -20815,7 +20777,6 @@
|
||||
"chai": "^4.3.7",
|
||||
"chai-string": "^1.5.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"handlebars": "^4.7.8",
|
||||
"json-query": "^2.2.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
@ -25207,6 +25168,7 @@
|
||||
"@usebruno/js": {
|
||||
"version": "file:packages/bruno-js",
|
||||
"requires": {
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/query": "0.1.0",
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
@ -25216,7 +25178,6 @@
|
||||
"chai": "^4.3.7",
|
||||
"chai-string": "^1.5.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"handlebars": "^4.7.8",
|
||||
"json-query": "^2.2.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
@ -26162,6 +26123,7 @@
|
||||
"graphql": "^16.6.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"is-valid-path": "^0.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-bigint": "^1.0.0",
|
||||
@ -29284,21 +29246,6 @@
|
||||
"graphql-ws": {
|
||||
"version": "5.12.1"
|
||||
},
|
||||
"handlebars": {
|
||||
"version": "4.7.8",
|
||||
"requires": {
|
||||
"minimist": "^1.2.5",
|
||||
"neo-async": "^2.6.2",
|
||||
"source-map": "^0.6.1",
|
||||
"uglify-js": "^3.1.4",
|
||||
"wordwrap": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "1.2.8"
|
||||
}
|
||||
}
|
||||
},
|
||||
"har-schema": {
|
||||
"version": "2.0.0"
|
||||
},
|
||||
@ -29543,7 +29490,6 @@
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
}
|
||||
@ -31169,7 +31115,8 @@
|
||||
"version": "0.6.3"
|
||||
},
|
||||
"neo-async": {
|
||||
"version": "2.6.2"
|
||||
"version": "2.6.2",
|
||||
"dev": true
|
||||
},
|
||||
"new-github-issue-url": {
|
||||
"version": "0.2.1"
|
||||
@ -33261,7 +33208,8 @@
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1"
|
||||
"version": "0.6.1",
|
||||
"dev": true
|
||||
},
|
||||
"source-map-js": {
|
||||
"version": "1.0.2"
|
||||
@ -34060,10 +34008,6 @@
|
||||
"uc.micro": {
|
||||
"version": "1.0.6"
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.17.4",
|
||||
"optional": true
|
||||
},
|
||||
"underscore": {
|
||||
"version": "1.6.0"
|
||||
},
|
||||
@ -34458,9 +34402,6 @@
|
||||
"version": "2.0.1",
|
||||
"dev": true
|
||||
},
|
||||
"wordwrap": {
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"requires": {
|
||||
|
@ -63,6 +63,9 @@ const Headers = ({ collection, folder }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Request headers that will be sent with every request inside this folder.
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -0,0 +1,13 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateFolderRequestScript, updateFolderResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
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 { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateFolderRequestScript({
|
||||
script: value,
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onResponseScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateFolderResponseScript({
|
||||
script: value,
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Pre and post-request scripts that will run before and after any request inside this folder is sent.
|
||||
</div>
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 mt-6">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-12">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Script;
|
@ -0,0 +1,46 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
table {
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
|
||||
li {
|
||||
background-color: ${(props) => props.theme.bg} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
@ -0,0 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div``;
|
||||
|
||||
export default StyledWrapper;
|
@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateFolderTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Tests = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tests = get(folder, 'root.request.tests', '');
|
||||
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
updateFolderTests({
|
||||
tests: value,
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<div className="text-xs mb-4 text-muted">These tests will run any time a request in this collection is sent.</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={tests || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tests;
|
@ -1,16 +1,24 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
|
||||
import { updatedFolderSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Headers from './Headers';
|
||||
import Script from './Script';
|
||||
import Tests from './Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const FolderSettings = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tab = folder?.settingsSelectedTab || 'headers';
|
||||
let tab = 'headers';
|
||||
const { folderLevelSettingsSelectedTab } = collection;
|
||||
if (folderLevelSettingsSelectedTab?.[folder.uid]) {
|
||||
tab = folderLevelSettingsSelectedTab[folder.uid];
|
||||
}
|
||||
|
||||
const setTab = (tab) => {
|
||||
dispatch(
|
||||
updateSettingsSelectedTab({
|
||||
collectionUid: folder.collectionUid,
|
||||
updatedFolderSettingsSelectedTab({
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid,
|
||||
tab
|
||||
})
|
||||
@ -22,7 +30,12 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
case 'headers': {
|
||||
return <Headers collection={collection} folder={folder} />;
|
||||
}
|
||||
// TODO: Add auth
|
||||
case 'script': {
|
||||
return <Script collection={collection} folder={folder} />;
|
||||
}
|
||||
case 'test': {
|
||||
return <Tests collection={collection} folder={folder} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -33,19 +46,22 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full relative px-4 py-4">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
|
||||
Headers
|
||||
<StyledWrapper>
|
||||
<div className="flex flex-col h-full relative px-4 py-4">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => setTab('script')}>
|
||||
Script
|
||||
</div>
|
||||
<div className={getTabClassname('test')} role="tab" onClick={() => setTab('test')}>
|
||||
Test
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
</div> */}
|
||||
<section className={`flex mt-4 h-full`}>{getTabPanel(tab)}</section>
|
||||
</div>
|
||||
<section className={`flex ${['auth', 'script', 'docs', 'clientCert'].includes(tab) ? '' : 'mt-4'}`}>
|
||||
{getTabPanel(tab)}
|
||||
</section>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -44,7 +44,7 @@ const CollectionToolBar = ({ collection }) => {
|
||||
<div className="flex items-center p-2">
|
||||
<div className="flex flex-1 items-center cursor-pointer hover:underline" onClick={viewCollectionSettings}>
|
||||
<IconFiles size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2 mr-4 font-semibold">{collection?.name || 'Folder'}</span>
|
||||
<span className="ml-2 mr-4 font-semibold">{collection?.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-end">
|
||||
<span className="mr-2">
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { IconVariable, IconSettings, IconRun } from '@tabler/icons';
|
||||
import { IconVariable, IconSettings, IconRun, IconFolder } from '@tabler/icons';
|
||||
|
||||
const SpecialTab = ({ handleCloseClick, type, folderName }) => {
|
||||
const getTabInfo = (type, folderName) => {
|
||||
const SpecialTab = ({ handleCloseClick, type, tabName }) => {
|
||||
const getTabInfo = (type, tabName) => {
|
||||
switch (type) {
|
||||
case 'collection-settings': {
|
||||
return (
|
||||
@ -15,8 +15,8 @@ const SpecialTab = ({ handleCloseClick, type, folderName }) => {
|
||||
case 'folder-settings': {
|
||||
return (
|
||||
<div className="flex items-center flex-nowrap overflow-hidden">
|
||||
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600 min-w-[18px]" />
|
||||
<span className="ml-1 leading-6 truncate">{folderName || 'Folder'}</span>
|
||||
<IconFolder size={18} strokeWidth={1.5} className="text-yellow-600 min-w-[18px]" />
|
||||
<span className="ml-1 leading-6 truncate">{tabName || 'Folder'}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -41,7 +41,7 @@ const SpecialTab = ({ handleCloseClick, type, folderName }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center tab-label pl-2">{getTabInfo(type, folderName)}</div>
|
||||
<div className="flex items-center tab-label pl-2">{getTabInfo(type, tabName)}</div>
|
||||
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
||||
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
|
||||
<path
|
||||
|
@ -84,7 +84,11 @@ const RequestTab = ({ tab, collection, folderUid }) => {
|
||||
if (['collection-settings', 'folder-settings', 'variables', 'collection-runner'].includes(tab.type)) {
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} folderName={folder?.name} />
|
||||
{tab.type === 'folder-settings' ? (
|
||||
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} tabName={folder?.name} />
|
||||
) : (
|
||||
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} />
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import {
|
||||
collectionUnlinkFileEvent,
|
||||
processEnvUpdateEvent,
|
||||
runFolderEvent,
|
||||
folderAddFileEvent,
|
||||
runRequestEvent,
|
||||
scriptEnvironmentUpdateEvent
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
@ -49,13 +48,6 @@ const useIpcEvents = () => {
|
||||
})
|
||||
);
|
||||
}
|
||||
if (type === 'addFileDir') {
|
||||
dispatch(
|
||||
folderAddFileEvent({
|
||||
file: val
|
||||
})
|
||||
);
|
||||
}
|
||||
if (type === 'change') {
|
||||
dispatch(
|
||||
collectionChangeFileEvent({
|
||||
|
@ -15,7 +15,6 @@ import {
|
||||
getItemsToResequence,
|
||||
isItemAFolder,
|
||||
refreshUidsInItem,
|
||||
findItemInCollectionByPathname,
|
||||
isItemARequest,
|
||||
moveCollectionItem,
|
||||
moveCollectionItemToRootOfCollection,
|
||||
@ -42,6 +41,7 @@ import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { resolveRequestFilename } from 'utils/common/platform';
|
||||
import { parseQueryParams, splitOnFirst } from 'utils/url/index';
|
||||
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
|
||||
import { name } from 'file-loader';
|
||||
|
||||
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
@ -157,11 +157,19 @@ export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState)
|
||||
if (!folder) {
|
||||
return reject(new Error('Folder not found'));
|
||||
}
|
||||
console.log(collection);
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
const folderData = {
|
||||
name: folder.name,
|
||||
pathname: folder.pathname,
|
||||
root: folder.root
|
||||
};
|
||||
console.log(folderData);
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-folder-root', folder.pathname, folder.root)
|
||||
.invoke('renderer:save-folder-root', folderData)
|
||||
.then(() => toast.success('Folder Settings saved successfully'))
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
@ -171,37 +179,6 @@ export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState)
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
@ -247,25 +224,7 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);
|
||||
const itemTree = retrieveDirectoriesBetween(itemCopy.pathname, collectionCopy.name, itemCopy.filename);
|
||||
|
||||
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)
|
||||
sendNetworkRequest(itemCopy, collectionCopy, environment, collectionCopy.collectionVariables)
|
||||
.then((response) => {
|
||||
return dispatch(
|
||||
responseReceived({
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { uuid } from 'utils/common';
|
||||
import path from 'path';
|
||||
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, debounce } from 'lodash';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import {
|
||||
@ -33,6 +34,8 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
collection.settingsSelectedTab = 'headers';
|
||||
|
||||
collection.folderLevelSettingsSelectedTab = {};
|
||||
|
||||
// TODO: move this to use the nextAction approach
|
||||
// last action is used to track the last action performed on the collection
|
||||
// this is optional
|
||||
@ -97,6 +100,19 @@ export const collectionsSlice = createSlice({
|
||||
collection.settingsSelectedTab = tab;
|
||||
}
|
||||
},
|
||||
updatedFolderSettingsSelectedTab: (state, action) => {
|
||||
const { collectionUid, folderUid, tab } = action.payload;
|
||||
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const folder = findItemInCollection(collection, folderUid);
|
||||
|
||||
if (folder) {
|
||||
collection.folderLevelSettingsSelectedTab[folderUid] = tab;
|
||||
}
|
||||
}
|
||||
},
|
||||
collectionUnlinkEnvFileEvent: (state, action) => {
|
||||
const { data: environment, meta } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, meta.collectionUid);
|
||||
@ -1152,6 +1168,27 @@ export const collectionsSlice = createSlice({
|
||||
set(folder, 'root.request.headers', headers);
|
||||
}
|
||||
},
|
||||
updateFolderRequestScript: (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.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);
|
||||
}
|
||||
},
|
||||
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);
|
||||
}
|
||||
},
|
||||
addCollectionHeader: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@ -1190,21 +1227,10 @@ 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 isFolderRoot = file.meta.folderRoot ? true : false;
|
||||
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
|
||||
if (isCollectionRoot) {
|
||||
if (collection) {
|
||||
@ -1213,6 +1239,15 @@ export const collectionsSlice = createSlice({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFolderRoot) {
|
||||
const folderPath = path.dirname(file.meta.pathname);
|
||||
const folderItem = findItemInCollectionByPathname(collection, folderPath);
|
||||
if (folderItem) {
|
||||
folderItem.root = file.data;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
const dirname = getDirectoryName(file.meta.pathname);
|
||||
const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dirname);
|
||||
@ -1523,6 +1558,7 @@ export const {
|
||||
sortCollections,
|
||||
updateLastAction,
|
||||
updateSettingsSelectedTab,
|
||||
updatedFolderSettingsSelectedTab,
|
||||
collectionUnlinkEnvFileEvent,
|
||||
saveEnvironment,
|
||||
selectEnvironment,
|
||||
@ -1573,6 +1609,9 @@ export const {
|
||||
addFolderHeader,
|
||||
updateFolderHeader,
|
||||
deleteFolderHeader,
|
||||
updateFolderRequestScript,
|
||||
updateFolderResponseScript,
|
||||
updateFolderTests,
|
||||
addCollectionHeader,
|
||||
updateCollectionHeader,
|
||||
deleteCollectionHeader,
|
||||
@ -1589,7 +1628,6 @@ export const {
|
||||
collectionUnlinkDirectoryEvent,
|
||||
collectionAddEnvFileEvent,
|
||||
collectionRenamedEvent,
|
||||
folderAddFileEvent,
|
||||
resetRunResults,
|
||||
runRequestEvent,
|
||||
runFolderEvent,
|
||||
|
@ -43,39 +43,6 @@ const isCollectionRootBruFile = (pathname, collectionPath) => {
|
||||
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);
|
||||
|
||||
@ -257,27 +224,38 @@ 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;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Is this a folder.bru file?
|
||||
if (path.basename(pathname) === 'folder.bru') {
|
||||
console.log('folder.bru file detected');
|
||||
const file = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name: path.basename(pathname),
|
||||
folderRoot: true
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
file.data = collectionBruToJson(bruContent);
|
||||
|
||||
hydrateBruCollectionFileWithUuid(file.data);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
return;
|
||||
@ -402,6 +380,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
|
||||
|
||||
const bru = fs.readFileSync(pathname, 'utf8');
|
||||
file.data = bruToJson(bru);
|
||||
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
win.webContents.send('main:collection-tree-updated', 'change', file);
|
||||
} catch (err) {
|
||||
|
@ -24,6 +24,16 @@ const collectionBruToJson = (bru) => {
|
||||
docs: _.get(json, 'docs', '')
|
||||
};
|
||||
|
||||
// add meta if it exists
|
||||
// this is only for folder bru file
|
||||
// in the future, all of this will be replaced by standard bru lang
|
||||
if (json.meta) {
|
||||
transformedJson.meta = {
|
||||
name: json.meta.name,
|
||||
seq: json.meta.seq
|
||||
};
|
||||
}
|
||||
|
||||
return transformedJson;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
@ -48,6 +58,16 @@ const jsonToCollectionBru = (json) => {
|
||||
docs: _.get(json, 'docs', '')
|
||||
};
|
||||
|
||||
// add meta if it exists
|
||||
// this is only for folder bru file
|
||||
// in the future, all of this will be replaced by standard bru lang
|
||||
if (json.meta) {
|
||||
collectionBruJson.meta = {
|
||||
name: json.meta.name,
|
||||
seq: json.meta.seq
|
||||
};
|
||||
}
|
||||
|
||||
return _jsonToCollectionBru(collectionBruJson);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
|
@ -152,11 +152,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:save-folder-root', async (event, folderPathname, folderRoot) => {
|
||||
ipcMain.handle('renderer:save-folder-root', async (event, folder) => {
|
||||
try {
|
||||
const { name: folderName, root: folderRoot, pathname: folderPathname } = folder;
|
||||
const folderBruFilePath = path.join(folderPathname, 'folder.bru');
|
||||
|
||||
const content = jsonToBru(folderRoot);
|
||||
folderRoot.meta = {
|
||||
name: folderName
|
||||
};
|
||||
|
||||
const content = jsonToCollectionBru(folderRoot);
|
||||
await writeFile(folderBruFilePath, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
|
@ -461,8 +461,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const _request = item.draft ? item.draft.request : item.request;
|
||||
const request = prepareRequest(_request, collectionRoot, collectionPath);
|
||||
const request = prepareRequest(item, collection);
|
||||
const envVars = getEnvVars(environment);
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
@ -900,8 +899,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
...eventData
|
||||
});
|
||||
|
||||
const _request = item.draft ? item.draft.request : item.request;
|
||||
const request = prepareRequest(_request, collectionRoot, collectionPath);
|
||||
const request = prepareRequest(item, collection);
|
||||
const requestUid = uuid();
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
|
||||
|
@ -1,9 +1,79 @@
|
||||
const { get, each, filter, extend } = require('lodash');
|
||||
const os = require('os');
|
||||
const { get, each, filter, extend, compact } = require('lodash');
|
||||
const decomment = require('decomment');
|
||||
var JSONbig = require('json-bigint');
|
||||
const FormData = require('form-data');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getTreePathFromCollectionToItem } = require('../../utils/collection');
|
||||
|
||||
const mergeFolderLevelHeaders = (request, requestTreePath) => {
|
||||
let folderHeaders = new Map();
|
||||
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let headers = get(i, 'root.request.headers', []);
|
||||
headers.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
folderHeaders.set(header.name, header.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mergedFolderHeaders = Array.from(folderHeaders, ([name, value]) => ({ name, value, enabled: true }));
|
||||
let requestHeaders = request.headers || [];
|
||||
let requestHeadersMap = new Map();
|
||||
|
||||
for (let header of requestHeaders) {
|
||||
if (header.enabled) {
|
||||
requestHeadersMap.set(header.name, header.value);
|
||||
}
|
||||
}
|
||||
|
||||
mergedFolderHeaders.forEach((header) => {
|
||||
requestHeadersMap.set(header.name, header.value);
|
||||
});
|
||||
|
||||
request.headers = Array.from(requestHeadersMap, ([name, value]) => ({ name, value, enabled: true }));
|
||||
};
|
||||
|
||||
const mergeFolderLevelScripts = (request, requestTreePath) => {
|
||||
let folderCombinedPreReqScript = [];
|
||||
let folderCombinedPostResScript = [];
|
||||
let folderCombinedTests = [];
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let preReqScript = get(i, 'root.request.script.req', '');
|
||||
if (preReqScript && preReqScript.trim() !== '') {
|
||||
folderCombinedPreReqScript.push(preReqScript);
|
||||
}
|
||||
|
||||
let postResScript = get(i, 'root.request.script.res', '');
|
||||
if (postResScript && postResScript.trim() !== '') {
|
||||
folderCombinedPostResScript.push(postResScript);
|
||||
}
|
||||
|
||||
let tests = get(i, 'root.request.tests', []);
|
||||
if (tests && tests?.trim() !== '') {
|
||||
folderCombinedTests.push(tests);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (folderCombinedPreReqScript.length) {
|
||||
request.script.req = compact([...folderCombinedPreReqScript, request?.script?.req || '']).join(os.EOL);
|
||||
console.log('request.script.req', request.script.req);
|
||||
}
|
||||
|
||||
if (folderCombinedPostResScript.length) {
|
||||
request.script.res = compact([request?.script?.res || '', ...folderCombinedPostResScript.reverse()]).join(os.EOL);
|
||||
}
|
||||
|
||||
if (folderCombinedTests.length) {
|
||||
request.tests = compact([request?.tests || '', ...folderCombinedTests.reverse()]).join(os.EOL);
|
||||
}
|
||||
};
|
||||
|
||||
const parseFormData = (datas, collectionPath) => {
|
||||
// make axios work in node using form data
|
||||
@ -133,7 +203,10 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
return axiosRequest;
|
||||
};
|
||||
|
||||
const prepareRequest = (request, collectionRoot, collectionPath) => {
|
||||
const prepareRequest = (item, collection) => {
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const collectionPath = collection.pathname;
|
||||
const headers = {};
|
||||
let contentTypeDefined = false;
|
||||
let url = request.url;
|
||||
@ -148,6 +221,12 @@ const prepareRequest = (request, collectionRoot, collectionPath) => {
|
||||
}
|
||||
});
|
||||
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
if (requestTreePath && requestTreePath.length > 0) {
|
||||
mergeFolderLevelHeaders(request, requestTreePath);
|
||||
mergeFolderLevelScripts(request, requestTreePath);
|
||||
}
|
||||
|
||||
each(request.headers, (h) => {
|
||||
if (h.enabled && h.name.length > 0) {
|
||||
headers[h.name] = h.value;
|
||||
|
57
packages/bruno-electron/src/utils/collection.js
Normal file
57
packages/bruno-electron/src/utils/collection.js
Normal file
@ -0,0 +1,57 @@
|
||||
const each = require('lodash/each');
|
||||
const find = require('lodash/find');
|
||||
|
||||
const flattenItems = (items = []) => {
|
||||
const flattenedItems = [];
|
||||
|
||||
const flatten = (itms, flattened) => {
|
||||
each(itms, (i) => {
|
||||
flattened.push(i);
|
||||
|
||||
if (i.items && i.items.length) {
|
||||
flatten(i.items, flattened);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
flatten(items, flattenedItems);
|
||||
|
||||
return flattenedItems;
|
||||
};
|
||||
|
||||
const findItem = (items = [], itemUid) => {
|
||||
return find(items, (i) => i.uid === itemUid);
|
||||
};
|
||||
|
||||
const findItemInCollection = (collection, itemUid) => {
|
||||
let flattenedItems = flattenItems(collection.items);
|
||||
|
||||
return findItem(flattenedItems, itemUid);
|
||||
};
|
||||
|
||||
const findParentItemInCollection = (collection, itemUid) => {
|
||||
let flattenedItems = flattenItems(collection.items);
|
||||
|
||||
return find(flattenedItems, (item) => {
|
||||
return item.items && find(item.items, (i) => i.uid === itemUid);
|
||||
});
|
||||
};
|
||||
|
||||
const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _item.uid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item.uid);
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
flattenItems,
|
||||
findItem,
|
||||
findItemInCollection,
|
||||
findParentItemInCollection,
|
||||
getTreePathFromCollectionToItem
|
||||
};
|
Loading…
Reference in New Issue
Block a user