feat: folder level variables (#2530)

This commit is contained in:
lohit 2024-07-01 12:52:08 +05:30 committed by GitHub
parent ede29e29bd
commit 797306f10c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 433 additions and 12 deletions

View File

@ -0,0 +1,9 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.title {
color: var(--color-tab-inactive);
}
`;
export default StyledWrapper;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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,

View File

@ -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', '')

View File

@ -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();

View File

@ -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) => {

View File

@ -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);
});
});