mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-21 15:33:11 +01:00
feat: folder level variables (#2530)
This commit is contained in:
parent
ede29e29bd
commit
797306f10c
@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
@ -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-var {
|
||||
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;
|
@ -0,0 +1,161 @@
|
||||
import React from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
import { addFolderVar, deleteFolderVar, updateFolderVar } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const VarsTable = ({ folder, collection, vars, varType }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const addVar = () => {
|
||||
dispatch(
|
||||
addFolderVar({
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid,
|
||||
type: varType
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
const handleVarChange = (e, v, type) => {
|
||||
const _var = cloneDeep(v);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
const value = e.target.value;
|
||||
|
||||
if (variableNameRegex.test(value) === false) {
|
||||
toast.error(
|
||||
'Variable contains invalid characters! Variables must only contain alpha-numeric characters, "-", "_", "."'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_var.name = value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
_var.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
_var.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
dispatch(
|
||||
updateFolderVar({
|
||||
type: varType,
|
||||
var: _var,
|
||||
folderUid: folder.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveVar = (_var) => {
|
||||
dispatch(
|
||||
deleteFolderVar({
|
||||
type: varType,
|
||||
varUid: _var.uid,
|
||||
folderUid: folder.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
{varType === 'request' ? (
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Value</span>
|
||||
<Tooltip text="You can write any valid JS Template Literal here" tooltipId="request-var" />
|
||||
</div>
|
||||
</td>
|
||||
) : (
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<Tooltip text="You can write any valid JS expression here" tooltipId="response-var" />
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{vars && vars.length
|
||||
? vars.map((_var) => {
|
||||
return (
|
||||
<tr key={_var.uid}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={_var.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'name')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={_var.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
handleVarChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
_var,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={_var.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'enabled')}
|
||||
/>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveVar(_var)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-var text-link pr-2 py-3 mt-2 select-none" onClick={addVar}>
|
||||
+ Add
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default VarsTable;
|
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
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 handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<VarsTable folder={folder} collection={collection} vars={requestVars} varType="request" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<VarsTable folder={folder} collection={collection} vars={responseVars} varType="response" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Vars;
|
@ -6,6 +6,7 @@ import Headers from './Headers';
|
||||
import Script from './Script';
|
||||
import Tests from './Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars';
|
||||
|
||||
const FolderSettings = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -36,6 +37,9 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
case 'test': {
|
||||
return <Tests collection={collection} folder={folder} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars collection={collection} folder={folder} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -58,6 +62,9 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
<div className={getTabClassname('test')} role="tab" onClick={() => setTab('test')}>
|
||||
Test
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => setTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
</div>
|
||||
<section className={`flex mt-4 h-full`}>{getTabPanel(tab)}</section>
|
||||
</div>
|
||||
|
@ -1168,6 +1168,78 @@ export const collectionsSlice = createSlice({
|
||||
set(folder, 'root.request.headers', headers);
|
||||
}
|
||||
},
|
||||
addFolderVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
const type = action.payload.type;
|
||||
if (folder) {
|
||||
if (type === 'request') {
|
||||
const vars = get(folder, 'root.request.vars.req', []);
|
||||
vars.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'request',
|
||||
enabled: true
|
||||
});
|
||||
set(folder, 'root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
const vars = get(folder, 'root.request.vars.res', []);
|
||||
vars.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'response',
|
||||
enabled: true
|
||||
});
|
||||
set(folder, 'root.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateFolderVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
const type = action.payload.type;
|
||||
if (folder) {
|
||||
if (type === 'request') {
|
||||
let vars = get(folder, '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(folder, 'root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
let vars = get(folder, '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(folder, 'root.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteFolderVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
const type = action.payload.type;
|
||||
if (folder) {
|
||||
if (type === 'request') {
|
||||
let vars = get(folder, 'root.request.vars.req', []);
|
||||
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
|
||||
set(folder, 'root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
let vars = get(folder, 'root.request.vars.res', []);
|
||||
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
|
||||
set(folder, 'root.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateFolderRequestScript: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
@ -1609,6 +1681,9 @@ export const {
|
||||
addFolderHeader,
|
||||
updateFolderHeader,
|
||||
deleteFolderHeader,
|
||||
addFolderVar,
|
||||
updateFolderVar,
|
||||
deleteFolderVar,
|
||||
updateFolderRequestScript,
|
||||
updateFolderResponseScript,
|
||||
updateFolderTests,
|
||||
|
@ -52,7 +52,7 @@ const jsonToCollectionBru = (json) => {
|
||||
},
|
||||
vars: {
|
||||
req: _.get(json, 'request.vars.req', []),
|
||||
res: _.get(json, 'request.vars.req', [])
|
||||
res: _.get(json, 'request.vars.res', [])
|
||||
},
|
||||
tests: _.get(json, 'request.tests', ''),
|
||||
docs: _.get(json, 'docs', '')
|
||||
|
@ -418,7 +418,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// run post-response script
|
||||
let scriptResult;
|
||||
const responseScript = compact([get(collectionRoot, 'request.script.res'), get(request, 'script.res')]).join(
|
||||
const responseScript = compact([get(request, 'script.res'), get(collectionRoot, 'request.script.res')]).join(
|
||||
os.EOL
|
||||
);
|
||||
if (responseScript?.length) {
|
||||
@ -602,8 +602,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// run tests
|
||||
const testFile = compact([
|
||||
get(collectionRoot, 'request.tests'),
|
||||
item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests')
|
||||
item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests'),
|
||||
get(collectionRoot, 'request.tests')
|
||||
]).join(os.EOL);
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime();
|
||||
|
@ -18,6 +18,13 @@ const mergeFolderLevelHeaders = (request, requestTreePath) => {
|
||||
folderHeaders.set(header.name, header.value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let headers = get(i, 'request.headers', []);
|
||||
headers.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
folderHeaders.set(header.name, header.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,10 +45,84 @@ const mergeFolderLevelHeaders = (request, requestTreePath) => {
|
||||
request.headers = Array.from(requestHeadersMap, ([name, value]) => ({ name, value, enabled: true }));
|
||||
};
|
||||
|
||||
const mergeFolderLevelVars = (request, requestTreePath) => {
|
||||
let folderReqVars = new Map();
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
folderReqVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let vars = get(i, 'request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
folderReqVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
let mergedFolderReqVars = Array.from(folderReqVars, ([name, value]) => ({ name, value, enabled: true }));
|
||||
let requestReqVars = request?.vars?.req || [];
|
||||
let requestReqVarsMap = new Map();
|
||||
for (let _var of requestReqVars) {
|
||||
if (_var.enabled) {
|
||||
requestReqVarsMap.set(_var.name, _var.value);
|
||||
}
|
||||
}
|
||||
mergedFolderReqVars.forEach((_var) => {
|
||||
requestReqVarsMap.set(_var.name, _var.value);
|
||||
});
|
||||
request.vars.req = Array.from(requestReqVarsMap, ([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
enabled: true,
|
||||
type: 'request'
|
||||
}));
|
||||
|
||||
let folderResVars = new Map();
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.res', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
folderResVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let vars = get(i, 'request.vars.res', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
folderResVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
let mergedFolderResVars = Array.from(folderResVars, ([name, value]) => ({ name, value, enabled: true }));
|
||||
let requestResVars = request?.vars?.res || [];
|
||||
let requestResVarsMap = new Map();
|
||||
for (let _var of requestResVars) {
|
||||
if (_var.enabled) {
|
||||
requestResVarsMap.set(_var.name, _var.value);
|
||||
}
|
||||
}
|
||||
mergedFolderResVars.forEach((_var) => {
|
||||
requestResVarsMap.set(_var.name, _var.value);
|
||||
});
|
||||
request.vars.res = Array.from(requestResVarsMap, ([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
enabled: true,
|
||||
type: 'response'
|
||||
}));
|
||||
};
|
||||
|
||||
const mergeFolderLevelScripts = (request, requestTreePath) => {
|
||||
let folderCombinedPreReqScript = [];
|
||||
let folderCombinedPostResScript = [];
|
||||
let folderCombinedTests = [];
|
||||
let folderCombinedTests = '';
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let preReqScript = get(i, 'root.request.script.req', '');
|
||||
@ -54,16 +135,15 @@ const mergeFolderLevelScripts = (request, requestTreePath) => {
|
||||
folderCombinedPostResScript.push(postResScript);
|
||||
}
|
||||
|
||||
let tests = get(i, 'root.request.tests', []);
|
||||
if (tests && tests?.trim() !== '') {
|
||||
folderCombinedTests.push(tests);
|
||||
let tests = get(i, 'root.request.tests', '');
|
||||
if (tests?.trim?.() !== '') {
|
||||
folderCombinedTests = `${folderCombinedTests} \n ${tests} \n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -71,7 +151,7 @@ const mergeFolderLevelScripts = (request, requestTreePath) => {
|
||||
}
|
||||
|
||||
if (folderCombinedTests.length) {
|
||||
request.tests = compact([request?.tests || '', ...folderCombinedTests.reverse()]).join(os.EOL);
|
||||
request.tests = `${request?.tests} \n ${folderCombinedTests}`;
|
||||
}
|
||||
};
|
||||
|
||||
@ -225,6 +305,7 @@ const prepareRequest = (item, collection) => {
|
||||
if (requestTreePath && requestTreePath.length > 0) {
|
||||
mergeFolderLevelHeaders(request, requestTreePath);
|
||||
mergeFolderLevelScripts(request, requestTreePath);
|
||||
mergeFolderLevelVars(request, requestTreePath);
|
||||
}
|
||||
|
||||
each(request.headers, (h) => {
|
||||
|
@ -7,14 +7,14 @@ describe('prepare-request: prepareRequest', () => {
|
||||
it('If request body is valid JSON', async () => {
|
||||
const body = { mode: 'json', json: '{\n"test": "{{someVar}}" // comment\n}' };
|
||||
const expected = { test: '{{someVar}}' };
|
||||
const result = prepareRequest({ body });
|
||||
const result = prepareRequest({ request: { body } }, {});
|
||||
expect(result.data).toEqual(expected);
|
||||
});
|
||||
|
||||
it('If request body is not valid JSON', async () => {
|
||||
const body = { mode: 'json', json: '{\n"test": {{someVar}} // comment\n}' };
|
||||
const expected = '{\n"test": {{someVar}} \n}';
|
||||
const result = prepareRequest({ body });
|
||||
const result = prepareRequest({ request: { body } }, {});
|
||||
expect(result.data).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user