mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
feat: Tagging requests and filtering collection runs using tags
This commit is contained in:
committed by
lohit-bruno
parent
ecc6c1604c
commit
3803576aa4
@@ -19,6 +19,7 @@ import StyledWrapper from './StyledWrapper';
|
|||||||
import Documentation from 'components/Documentation/index';
|
import Documentation from 'components/Documentation/index';
|
||||||
import GraphQLSchemaActions from '../GraphQLSchemaActions/index';
|
import GraphQLSchemaActions from '../GraphQLSchemaActions/index';
|
||||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||||
|
import Tags from 'components/RequestPane/Tags/index';
|
||||||
|
|
||||||
const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
|
const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -101,6 +102,9 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
|
|||||||
case 'docs': {
|
case 'docs': {
|
||||||
return <Documentation item={item} collection={collection} />;
|
return <Documentation item={item} collection={collection} />;
|
||||||
}
|
}
|
||||||
|
case 'tags': {
|
||||||
|
return <Tags item={item} collection={collection} />;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return <div className="mt-4">404 | Not found</div>;
|
return <div className="mt-4">404 | Not found</div>;
|
||||||
}
|
}
|
||||||
@@ -152,6 +156,9 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
|
|||||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||||
Docs
|
Docs
|
||||||
</div>
|
</div>
|
||||||
|
<div className={getTabClassname('tags')} role="tab" onClick={() => selectTab('tags')}>
|
||||||
|
Tags
|
||||||
|
</div>
|
||||||
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
||||||
</div>
|
</div>
|
||||||
<section className="flex w-full mt-5 flex-1 relative">
|
<section className="flex w-full mt-5 flex-1 relative">
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import HeightBoundContainer from 'ui/HeightBoundContainer';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import StatusDot from 'components/StatusDot';
|
import StatusDot from 'components/StatusDot';
|
||||||
import Settings from 'components/RequestPane/Settings';
|
import Settings from 'components/RequestPane/Settings';
|
||||||
|
import Tags from 'components/RequestPane/Tags/index';
|
||||||
|
|
||||||
const HttpRequestPane = ({ item, collection }) => {
|
const HttpRequestPane = ({ item, collection }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -65,6 +66,9 @@ const HttpRequestPane = ({ item, collection }) => {
|
|||||||
case 'settings': {
|
case 'settings': {
|
||||||
return <Settings item={item} collection={collection} />;
|
return <Settings item={item} collection={collection} />;
|
||||||
}
|
}
|
||||||
|
case 'tags': {
|
||||||
|
return <Tags item={item} collection={collection} />;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return <div className="mt-4">404 | Not found</div>;
|
return <div className="mt-4">404 | Not found</div>;
|
||||||
}
|
}
|
||||||
@@ -165,6 +169,9 @@ const HttpRequestPane = ({ item, collection }) => {
|
|||||||
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
|
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
|
||||||
Settings
|
Settings
|
||||||
</div>
|
</div>
|
||||||
|
<div className={getTabClassname('tags')} role="tab" onClick={() => selectTab('tags')}>
|
||||||
|
Tags
|
||||||
|
</div>
|
||||||
{focusedTab.requestPaneTab === 'body' ? (
|
{focusedTab.requestPaneTab === 'body' ? (
|
||||||
<div className="flex flex-grow justify-end items-center">
|
<div className="flex flex-grow justify-end items-center">
|
||||||
<RequestBodyMode item={item} collection={collection} />
|
<RequestBodyMode item={item} collection={collection} />
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
input[type='text'] {
|
||||||
|
border: solid 1px transparent;
|
||||||
|
outline: none !important;
|
||||||
|
background-color: inherit;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border: solid 1px transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid ${(props) => props.theme.text};
|
||||||
|
border-radius: 5px;
|
||||||
|
padding-inline: 5px;
|
||||||
|
background: ${(props) => props.theme.sidebar.bg};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Wrapper;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { IconX } from '@tabler/icons';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
|
const TagList = ({ tags, onTagRemove, onTagAdd }) => {
|
||||||
|
const tagNameRegex = /^[\w-]+$/;
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [text, setText] = useState('');
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
setText(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (e.code == 'Escape') {
|
||||||
|
setText('');
|
||||||
|
setIsEditing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.code !== 'Enter' && e.code !== 'Space') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!tagNameRegex.test(text)) {
|
||||||
|
toast.error('Tags must only contain alpha-numeric characters, "-", "_"');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tags.includes(text)) {
|
||||||
|
toast.error(`Tag "${text}" already exists`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onTagAdd(text);
|
||||||
|
setText('');
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper className="flex flex-wrap gap-2 mt-1">
|
||||||
|
<ul className="flex flex-wrap gap-1">
|
||||||
|
{tags && tags.length
|
||||||
|
? tags.map((_tag) => (
|
||||||
|
<li key={_tag}>
|
||||||
|
<span>{_tag}</span>
|
||||||
|
<button tabIndex={-1} onClick={() => onTagRemove(_tag)}>
|
||||||
|
<IconX strokeWidth={1.5} size={20} />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</ul>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Space or Enter to add tag"
|
||||||
|
value={text}
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<button className="text-link select-none" onClick={() => setIsEditing(true)}>
|
||||||
|
+ Add
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TagList;
|
||||||
43
packages/bruno-app/src/components/RequestPane/Tags/index.js
Normal file
43
packages/bruno-app/src/components/RequestPane/Tags/index.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import 'github-markdown-css/github-markdown.css';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import { addRequestTag, deleteRequestTag } from 'providers/ReduxStore/slices/collections';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import TagList from './TagList/TagList';
|
||||||
|
|
||||||
|
const Tags = ({ item, collection }) => {
|
||||||
|
const tags = item.draft ? get(item, 'draft.request.tags') : get(item, 'request.tags');
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleAdd = (_tag) => {
|
||||||
|
dispatch(
|
||||||
|
addRequestTag({
|
||||||
|
tag: _tag,
|
||||||
|
itemUid: item.uid,
|
||||||
|
collectionUid: collection.uid
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = (_tag) => {
|
||||||
|
dispatch(
|
||||||
|
deleteRequestTag({
|
||||||
|
tag: _tag,
|
||||||
|
itemUid: item.uid,
|
||||||
|
collectionUid: collection.uid
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TagList tags={tags} onTagRemove={handleRemove} onTagAdd={handleAdd} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tags;
|
||||||
@@ -9,6 +9,7 @@ import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, Ic
|
|||||||
import ResponsePane from './ResponsePane';
|
import ResponsePane from './ResponsePane';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import { areItemsLoading } from 'utils/collections';
|
import { areItemsLoading } from 'utils/collections';
|
||||||
|
import TagList from 'components/RequestPane/Tags/TagList/TagList';
|
||||||
|
|
||||||
const getDisplayName = (fullPath, pathname, name = '') => {
|
const getDisplayName = (fullPath, pathname, name = '') => {
|
||||||
let relativePath = path.relative(fullPath, pathname);
|
let relativePath = path.relative(fullPath, pathname);
|
||||||
@@ -42,6 +43,8 @@ export default function RunnerResults({ collection }) {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [selectedItem, setSelectedItem] = useState(null);
|
const [selectedItem, setSelectedItem] = useState(null);
|
||||||
const [delay, setDelay] = useState(null);
|
const [delay, setDelay] = useState(null);
|
||||||
|
const [tags, setTags] = useState({ include: [], exclude: [] });
|
||||||
|
const [tagsEnabled, setTagsEnabled] = useState(false);
|
||||||
|
|
||||||
// ref for the runner output body
|
// ref for the runner output body
|
||||||
const runnerBodyRef = useRef();
|
const runnerBodyRef = useRef();
|
||||||
@@ -88,11 +91,19 @@ export default function RunnerResults({ collection }) {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
const runCollection = () => {
|
const runCollection = () => {
|
||||||
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay)));
|
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags));
|
||||||
};
|
};
|
||||||
|
|
||||||
const runAgain = () => {
|
const runAgain = () => {
|
||||||
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive, Number(delay)));
|
dispatch(
|
||||||
|
runCollectionFolder(
|
||||||
|
collection.uid,
|
||||||
|
runnerInfo.folderUid,
|
||||||
|
runnerInfo.isRecursive,
|
||||||
|
Number(delay),
|
||||||
|
tagsEnabled && tags
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetRunner = () => {
|
const resetRunner = () => {
|
||||||
@@ -140,6 +151,37 @@ export default function RunnerResults({ collection }) {
|
|||||||
onChange={(e) => setDelay(e.target.value)}
|
onChange={(e) => setDelay(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-6 flex flex-col">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<label className="block font-medium">Filter requests with tags</label>
|
||||||
|
<input
|
||||||
|
className="cursor-pointer"
|
||||||
|
type="checkbox"
|
||||||
|
checked={tagsEnabled}
|
||||||
|
onChange={() => setTagsEnabled(!tagsEnabled)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{tagsEnabled && (
|
||||||
|
<div className="flex p-4 gap-4 max-w-xl justify-between">
|
||||||
|
<div className="w-1/2">
|
||||||
|
<span>Included tags:</span>
|
||||||
|
<TagList
|
||||||
|
tags={tags.include}
|
||||||
|
onTagAdd={(tag) => setTags({ ...tags, include: [...tags.include, tag] })}
|
||||||
|
onTagRemove={(tag) => setTags({ ...tags, include: tags.include.filter((t) => t !== tag) })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2">
|
||||||
|
<span>Excluded tags:</span>
|
||||||
|
<TagList
|
||||||
|
tags={tags.exclude}
|
||||||
|
onTagAdd={(tag) => setTags({ ...tags, exclude: [...tags.exclude, tag] })}
|
||||||
|
onTagRemove={(tag) => setTags({ ...tags, exclude: tags.exclude.filter((t) => t !== tag) })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runCollection}>
|
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runCollection}>
|
||||||
Run Collection
|
Run Collection
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { uuid } from 'utils/common';
|
import { uuid } from 'utils/common';
|
||||||
import Modal from 'components/Modal';
|
import Modal from 'components/Modal';
|
||||||
@@ -8,12 +8,15 @@ import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/act
|
|||||||
import { flattenItems } from 'utils/collections';
|
import { flattenItems } from 'utils/collections';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import { areItemsLoading } from 'utils/collections';
|
import { areItemsLoading } from 'utils/collections';
|
||||||
|
import TagList from 'components/RequestPane/Tags/TagList/TagList';
|
||||||
|
|
||||||
const RunCollectionItem = ({ collectionUid, item, onClose }) => {
|
const RunCollectionItem = ({ collectionUid, item, onClose }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
|
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
|
||||||
const isCollectionRunInProgress = collection?.runnerResult?.info?.status && (collection?.runnerResult?.info?.status !== 'ended');
|
const isCollectionRunInProgress = collection?.runnerResult?.info?.status && (collection?.runnerResult?.info?.status !== 'ended');
|
||||||
|
const [tags, setTags] = useState({ include: [], exclude: [] });
|
||||||
|
const [tagsEnabled, setTagsEnabled] = useState(false);
|
||||||
|
|
||||||
const onSubmit = (recursive) => {
|
const onSubmit = (recursive) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
@@ -24,7 +27,7 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
if (!isCollectionRunInProgress) {
|
if (!isCollectionRunInProgress) {
|
||||||
dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive));
|
dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive, 0, tagsEnabled && tags));
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@@ -71,6 +74,39 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => {
|
|||||||
<div className={isFolderLoading ? "mb-2" : "mb-8"}>This will run all the requests in this folder and all its subfolders.</div>
|
<div className={isFolderLoading ? "mb-2" : "mb-8"}>This will run all the requests in this folder and all its subfolders.</div>
|
||||||
{isFolderLoading ? <div className='mb-8 warning'>Requests in this folder are still loading.</div> : null}
|
{isFolderLoading ? <div className='mb-8 warning'>Requests in this folder are still loading.</div> : null}
|
||||||
{isCollectionRunInProgress ? <div className='mb-6 warning'>A Collection Run is already in progress.</div> : null}
|
{isCollectionRunInProgress ? <div className='mb-6 warning'>A Collection Run is already in progress.</div> : null}
|
||||||
|
|
||||||
|
<div className="mb-8 flex flex-col">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<label className="block font-medium">Filter requests with tags</label>
|
||||||
|
<input
|
||||||
|
className="cursor-pointer"
|
||||||
|
type="checkbox"
|
||||||
|
checked={tagsEnabled}
|
||||||
|
onChange={() => setTagsEnabled(!tagsEnabled)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{tagsEnabled && (
|
||||||
|
<div className="flex p-4 gap-4 max-w-xl justify-between">
|
||||||
|
<div className="w-1/2">
|
||||||
|
<span>Included tags:</span>
|
||||||
|
<TagList
|
||||||
|
tags={tags.include}
|
||||||
|
onTagAdd={(tag) => setTags({ ...tags, include: [...tags.include, tag] })}
|
||||||
|
onTagRemove={(tag) => setTags({ ...tags, include: tags.include.filter((t) => t !== tag) })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2">
|
||||||
|
<span>Excluded tags:</span>
|
||||||
|
<TagList
|
||||||
|
tags={tags.exclude}
|
||||||
|
onTagAdd={(tag) => setTags({ ...tags, exclude: [...tags.exclude, tag] })}
|
||||||
|
onTagRemove={(tag) => setTags({ ...tags, exclude: tags.exclude.filter((t) => t !== tag) })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end bruno-modal-footer">
|
<div className="flex justify-end bruno-modal-footer">
|
||||||
<span className="mr-3">
|
<span className="mr-3">
|
||||||
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ export const cancelRunnerExecution = (cancelTokenUid) => (dispatch) => {
|
|||||||
cancelNetworkRequest(cancelTokenUid).catch((err) => console.log(err));
|
cancelNetworkRequest(cancelTokenUid).catch((err) => console.log(err));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const runCollectionFolder = (collectionUid, folderUid, recursive, delay) => (dispatch, getState) => {
|
export const runCollectionFolder = (collectionUid, folderUid, recursive, delay, tags) => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
|
const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
|
||||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||||
@@ -354,7 +354,8 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive, delay)
|
|||||||
environment,
|
environment,
|
||||||
collectionCopy.runtimeVariables,
|
collectionCopy.runtimeVariables,
|
||||||
recursive,
|
recursive,
|
||||||
delay
|
delay,
|
||||||
|
tags
|
||||||
)
|
)
|
||||||
.then(resolve)
|
.then(resolve)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|||||||
@@ -2339,9 +2339,43 @@ export const collectionsSlice = createSlice({
|
|||||||
set(folder, 'root.request.auth', {});
|
set(folder, 'root.request.auth', {});
|
||||||
set(folder, 'root.request.auth.mode', action.payload.mode);
|
set(folder, 'root.request.auth.mode', action.payload.mode);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
|
||||||
|
|
||||||
|
addRequestTag: (state, action) => {
|
||||||
|
const { tag, collectionUid, itemUid } = action.payload;
|
||||||
|
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
const item = findItemInCollection(collection, itemUid);
|
||||||
|
|
||||||
|
if (item && isItemARequest(item)) {
|
||||||
|
if (!item.draft) {
|
||||||
|
item.draft = cloneDeep(item);
|
||||||
|
}
|
||||||
|
item.draft.request.tags = item.draft.request.tags || [];
|
||||||
|
if (!item.draft.request.tags.includes(tag.trim())) {
|
||||||
|
item.draft.request.tags.push(tag.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteRequestTag: (state, action) => {
|
||||||
|
const { tag, collectionUid, itemUid } = action.payload;
|
||||||
|
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
const item = findItemInCollection(collection, itemUid);
|
||||||
|
|
||||||
|
if (item && isItemARequest(item)) {
|
||||||
|
if (!item.draft) {
|
||||||
|
item.draft = cloneDeep(item);
|
||||||
|
}
|
||||||
|
item.draft.request.tags = item.draft.request.tags || [];
|
||||||
|
item.draft.request.tags = item.draft.request.tags.filter((t) => t !== tag.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
@@ -2457,6 +2491,8 @@ export const {
|
|||||||
collectionGetOauth2CredentialsByUrl,
|
collectionGetOauth2CredentialsByUrl,
|
||||||
updateFolderAuth,
|
updateFolderAuth,
|
||||||
updateFolderAuthMode,
|
updateFolderAuthMode,
|
||||||
|
addRequestTag,
|
||||||
|
deleteRequestTag
|
||||||
} = collectionsSlice.actions;
|
} = collectionsSlice.actions;
|
||||||
|
|
||||||
export default collectionsSlice.reducer;
|
export default collectionsSlice.reducer;
|
||||||
|
|||||||
@@ -257,7 +257,8 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
|||||||
vars: si.request.vars,
|
vars: si.request.vars,
|
||||||
assertions: si.request.assertions,
|
assertions: si.request.assertions,
|
||||||
tests: si.request.tests,
|
tests: si.request.tests,
|
||||||
docs: si.request.docs
|
docs: si.request.docs,
|
||||||
|
tags: si.request.tags
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle auth object dynamically
|
// Handle auth object dynamically
|
||||||
@@ -565,7 +566,8 @@ export const transformRequestToSaveToFilesystem = (item) => {
|
|||||||
vars: _item.request.vars,
|
vars: _item.request.vars,
|
||||||
assertions: _item.request.assertions,
|
assertions: _item.request.assertions,
|
||||||
tests: _item.request.tests,
|
tests: _item.request.tests,
|
||||||
docs: _item.request.docs
|
docs: _item.request.docs,
|
||||||
|
tags: _item.request.tags
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const { getRunnerSummary } = require('@usebruno/common/runner');
|
|||||||
const { exists, isFile, isDirectory } = require('../utils/filesystem');
|
const { exists, isFile, isDirectory } = require('../utils/filesystem');
|
||||||
const { runSingleRequest } = require('../runner/run-single-request');
|
const { runSingleRequest } = require('../runner/run-single-request');
|
||||||
const { bruToEnvJson, getEnvVars } = require('../utils/bru');
|
const { bruToEnvJson, getEnvVars } = require('../utils/bru');
|
||||||
|
const { isRequestTagsIncluded } = require("@usebruno/common")
|
||||||
const makeJUnitOutput = require('../reporters/junit');
|
const makeJUnitOutput = require('../reporters/junit');
|
||||||
const makeHtmlOutput = require('../reporters/html');
|
const makeHtmlOutput = require('../reporters/html');
|
||||||
const { rpad } = require('../utils/common');
|
const { rpad } = require('../utils/common');
|
||||||
@@ -199,6 +200,14 @@ const builder = async (yargs) => {
|
|||||||
type:"number",
|
type:"number",
|
||||||
description: "Delay between each requests (in miliseconds)"
|
description: "Delay between each requests (in miliseconds)"
|
||||||
})
|
})
|
||||||
|
.option('tags', {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Tags to include in the run'
|
||||||
|
})
|
||||||
|
.option('exclude-tags', {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Tags to exclude from the run'
|
||||||
|
})
|
||||||
.example('$0 run request.bru', 'Run a request')
|
.example('$0 run request.bru', 'Run a request')
|
||||||
.example('$0 run request.bru --env local', 'Run a request with the environment set to local')
|
.example('$0 run request.bru --env local', 'Run a request with the environment set to local')
|
||||||
.example('$0 run request.bru --env-file env.bru', 'Run a request with the environment from env.bru file')
|
.example('$0 run request.bru --env-file env.bru', 'Run a request with the environment from env.bru file')
|
||||||
@@ -241,7 +250,11 @@ const builder = async (yargs) => {
|
|||||||
)
|
)
|
||||||
.example('$0 run --client-cert-config client-cert-config.json', 'Run a request with Client certificate configurations')
|
.example('$0 run --client-cert-config client-cert-config.json', 'Run a request with Client certificate configurations')
|
||||||
.example('$0 run folder --delay delayInMs', 'Run a folder with given miliseconds delay between each requests.')
|
.example('$0 run folder --delay delayInMs', 'Run a folder with given miliseconds delay between each requests.')
|
||||||
.example('$0 run --noproxy', 'Run requests with system proxy disabled');
|
.example('$0 run --noproxy', 'Run requests with system proxy disabled')
|
||||||
|
.example(
|
||||||
|
'$0 run folder --tags=hello,world --exclude-tags=skip',
|
||||||
|
'Run only requests with tags "hello" or "world" and exclude any request with tag "skip".'
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handler = async function (argv) {
|
const handler = async function (argv) {
|
||||||
@@ -268,7 +281,9 @@ const handler = async function (argv) {
|
|||||||
reporterSkipHeaders,
|
reporterSkipHeaders,
|
||||||
clientCertConfig,
|
clientCertConfig,
|
||||||
noproxy,
|
noproxy,
|
||||||
delay
|
delay,
|
||||||
|
tags: includeTags,
|
||||||
|
excludeTags
|
||||||
} = argv;
|
} = argv;
|
||||||
const collectionPath = process.cwd();
|
const collectionPath = process.cwd();
|
||||||
|
|
||||||
@@ -353,7 +368,7 @@ const handler = async function (argv) {
|
|||||||
if (!match) {
|
if (!match) {
|
||||||
console.error(
|
console.error(
|
||||||
chalk.red(`Overridable environment variable not correct: use name=value - presented: `) +
|
chalk.red(`Overridable environment variable not correct: use name=value - presented: `) +
|
||||||
chalk.dim(`${value}`)
|
chalk.dim(`${value}`)
|
||||||
);
|
);
|
||||||
process.exit(constants.EXIT_STATUS.ERROR_INCORRECT_ENV_OVERRIDE);
|
process.exit(constants.EXIT_STATUS.ERROR_INCORRECT_ENV_OVERRIDE);
|
||||||
}
|
}
|
||||||
@@ -389,6 +404,9 @@ const handler = async function (argv) {
|
|||||||
}
|
}
|
||||||
options['ignoreTruststore'] = ignoreTruststore;
|
options['ignoreTruststore'] = ignoreTruststore;
|
||||||
|
|
||||||
|
includeTags = includeTags ? includeTags.split(',') : [];
|
||||||
|
excludeTags = excludeTags ? excludeTags.split(',') : [];
|
||||||
|
|
||||||
if (['json', 'junit', 'html'].indexOf(format) === -1) {
|
if (['json', 'junit', 'html'].indexOf(format) === -1) {
|
||||||
console.error(chalk.red(`Format must be one of "json", "junit or "html"`));
|
console.error(chalk.red(`Format must be one of "json", "junit or "html"`));
|
||||||
process.exit(constants.EXIT_STATUS.ERROR_INCORRECT_OUTPUT_FORMAT);
|
process.exit(constants.EXIT_STATUS.ERROR_INCORRECT_OUTPUT_FORMAT);
|
||||||
@@ -444,6 +462,10 @@ const handler = async function (argv) {
|
|||||||
console.error(chalk.red(`Path not found: ${resolvedPath}`));
|
console.error(chalk.red(`Path not found: ${resolvedPath}`));
|
||||||
process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND);
|
process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestItems = requestItems.filter((item) => {
|
||||||
|
return isRequestTagsIncluded(item.tags, includeTags, excludeTags);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
requestItems = getCallStack(resolvedPaths, collection, { recursive });
|
requestItems = getCallStack(resolvedPaths, collection, { recursive });
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ const bruToJson = (bru) => {
|
|||||||
name: _.get(json, 'meta.name'),
|
name: _.get(json, 'meta.name'),
|
||||||
seq: !_.isNaN(sequence) ? Number(sequence) : 1,
|
seq: !_.isNaN(sequence) ? Number(sequence) : 1,
|
||||||
settings: _.get(json, 'settings', {}),
|
settings: _.get(json, 'settings', {}),
|
||||||
|
tags: _.get(json, 'tags', []),
|
||||||
request: {
|
request: {
|
||||||
method: _.upperCase(_.get(json, 'http.method')),
|
method: _.upperCase(_.get(json, 'http.method')),
|
||||||
url: _.get(json, 'http.url'),
|
url: _.get(json, 'http.url'),
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export { mockDataFunctions } from './utils/faker-functions';
|
export { mockDataFunctions } from './utils/faker-functions';
|
||||||
export { default as interpolate } from './interpolate';
|
export {default as interpolate} from './interpolate';
|
||||||
|
export {default as isRequestTagsIncluded} from './tags';
|
||||||
|
|||||||
43
packages/bruno-common/src/tags/index.spec.ts
Normal file
43
packages/bruno-common/src/tags/index.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import isRequestTagsIncluded from './index';
|
||||||
|
|
||||||
|
describe('isRequestTagsIncluded', () => {
|
||||||
|
it('should include request when it has an included tag', () => {
|
||||||
|
const requestTags = ['tag1', 'tag2'];
|
||||||
|
const includeTags = ['tag1'];
|
||||||
|
const excludeTags: string[] = [];
|
||||||
|
const result = isRequestTagsIncluded(requestTags, includeTags, excludeTags);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include request when included tags is empty', () => {
|
||||||
|
const requestTags = ['tag1', 'tag2'];
|
||||||
|
const includeTags: string[] = [];
|
||||||
|
const excludeTags: string[] = [];
|
||||||
|
const result = isRequestTagsIncluded(requestTags, includeTags, excludeTags);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude request when it does not have an included tag', () => {
|
||||||
|
const requestTags = ['tag1'];
|
||||||
|
const includeTags = ['tag2'];
|
||||||
|
const excludeTags: string[] = [];
|
||||||
|
const result = isRequestTagsIncluded(requestTags, includeTags, excludeTags);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude request when it has an excluded tag', () => {
|
||||||
|
const requestTags = ['tag1'];
|
||||||
|
const includeTags: string[] = [];
|
||||||
|
const excludeTags = ['tag1'];
|
||||||
|
const result = isRequestTagsIncluded(requestTags, includeTags, excludeTags);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude request when it has both included and excluded tag', () => {
|
||||||
|
const requestTags = ['tag1', 'tag2'];
|
||||||
|
const includeTags: string[] = ['tag2'];
|
||||||
|
const excludeTags = ['tag1'];
|
||||||
|
const result = isRequestTagsIncluded(requestTags, includeTags, excludeTags);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
13
packages/bruno-common/src/tags/index.ts
Normal file
13
packages/bruno-common/src/tags/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* A request should be included if it has at least one tag that is included and no tags that are excluded
|
||||||
|
* @param requestTags Tags of the request
|
||||||
|
* @param includeTags Tags to include
|
||||||
|
* @param excludeTags Tags to exclude
|
||||||
|
*/
|
||||||
|
export const isRequestTagsIncluded = (requestTags: string[], includeTags: string[], excludeTags: string[]) => {
|
||||||
|
const shouldInclude = includeTags.length === 0 || requestTags.some((tag) => includeTags.includes(tag));
|
||||||
|
const shouldExclude = excludeTags.length > 0 && requestTags.some((tag) => excludeTags.includes(tag));
|
||||||
|
return shouldInclude && !shouldExclude;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default isRequestTagsIncluded;
|
||||||
@@ -149,7 +149,8 @@ const bruToJson = (data, parsed = false) => {
|
|||||||
vars: _.get(json, 'vars', {}),
|
vars: _.get(json, 'vars', {}),
|
||||||
assertions: _.get(json, 'assertions', []),
|
assertions: _.get(json, 'assertions', []),
|
||||||
tests: _.get(json, 'tests', ''),
|
tests: _.get(json, 'tests', ''),
|
||||||
docs: _.get(json, 'docs', '')
|
docs: _.get(json, 'docs', ''),
|
||||||
|
tags: _.get(json, 'tags', [])
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -215,7 +216,8 @@ const jsonToBru = async (json) => {
|
|||||||
assertions: _.get(json, 'request.assertions', []),
|
assertions: _.get(json, 'request.assertions', []),
|
||||||
tests: _.get(json, 'request.tests', ''),
|
tests: _.get(json, 'request.tests', ''),
|
||||||
settings: _.get(json, 'settings', {}),
|
settings: _.get(json, 'settings', {}),
|
||||||
docs: _.get(json, 'request.docs', '')
|
docs: _.get(json, 'request.docs', ''),
|
||||||
|
tags: _.get(json, 'request.tags', [])
|
||||||
};
|
};
|
||||||
|
|
||||||
const bru = jsonToBruV2(bruJson);
|
const bru = jsonToBruV2(bruJson);
|
||||||
@@ -257,7 +259,8 @@ const jsonToBruViaWorker = async (json) => {
|
|||||||
assertions: _.get(json, 'request.assertions', []),
|
assertions: _.get(json, 'request.assertions', []),
|
||||||
tests: _.get(json, 'request.tests', ''),
|
tests: _.get(json, 'request.tests', ''),
|
||||||
settings: _.get(json, 'settings', {}),
|
settings: _.get(json, 'settings', {}),
|
||||||
docs: _.get(json, 'request.docs', '')
|
docs: _.get(json, 'request.docs', ''),
|
||||||
|
tags: _.get(json, 'request.tags', [])
|
||||||
};
|
};
|
||||||
|
|
||||||
const bru = await bruParserWorker?.jsonToBru(bruJson)
|
const bru = await bruParserWorker?.jsonToBru(bruJson)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const { preferencesUtil } = require('../../store/preferences');
|
|||||||
const { getProcessEnvVars } = require('../../store/process-env');
|
const { getProcessEnvVars } = require('../../store/process-env');
|
||||||
const { getBrunoConfig } = require('../../store/bruno-config');
|
const { getBrunoConfig } = require('../../store/bruno-config');
|
||||||
const Oauth2Store = require('../../store/oauth2');
|
const Oauth2Store = require('../../store/oauth2');
|
||||||
|
const { isRequestTagsIncluded } = require('@usebruno/common');
|
||||||
|
|
||||||
const saveCookies = (url, headers) => {
|
const saveCookies = (url, headers) => {
|
||||||
if (preferencesUtil.shouldStoreCookies()) {
|
if (preferencesUtil.shouldStoreCookies()) {
|
||||||
@@ -952,7 +953,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'renderer:run-collection-folder',
|
'renderer:run-collection-folder',
|
||||||
async (event, folder, collection, environment, runtimeVariables, recursive, delay) => {
|
async (event, folder, collection, environment, runtimeVariables, recursive, delay, tags) => {
|
||||||
const collectionUid = collection.uid;
|
const collectionUid = collection.uid;
|
||||||
const collectionPath = collection.pathname;
|
const collectionPath = collection.pathname;
|
||||||
const folderUid = folder ? folder.uid : null;
|
const folderUid = folder ? folder.uid : null;
|
||||||
@@ -1012,6 +1013,15 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
folderRequests = sortByNameThenSequence(folderRequests)
|
folderRequests = sortByNameThenSequence(folderRequests)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter requests based on tags
|
||||||
|
if (tags && tags.include && tags.exclude) {
|
||||||
|
const includeTags = tags.include ? tags.include : [];
|
||||||
|
const excludeTags = tags.exclude ? tags.exclude : [];
|
||||||
|
folderRequests = folderRequests.filter(({ request }) => {
|
||||||
|
return isRequestTagsIncluded(request.tags, includeTags, excludeTags)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let currentRequestIndex = 0;
|
let currentRequestIndex = 0;
|
||||||
let nJumps = 0; // count the number of jumps to avoid infinite loops
|
let nJumps = 0; // count the number of jumps to avoid infinite loops
|
||||||
while (currentRequestIndex < folderRequests.length) {
|
while (currentRequestIndex < folderRequests.length) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const { safeParseJson, outdentString } = require('./utils');
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A Bru file is made up of blocks.
|
* A Bru file is made up of blocks.
|
||||||
* There are two types of blocks
|
* There are three types of blocks
|
||||||
*
|
*
|
||||||
* 1. Dictionary Blocks - These are blocks that have key value pairs
|
* 1. Dictionary Blocks - These are blocks that have key value pairs
|
||||||
* ex:
|
* ex:
|
||||||
@@ -19,6 +19,13 @@ const { safeParseJson, outdentString } = require('./utils');
|
|||||||
* "username": "John Nash",
|
* "username": "John Nash",
|
||||||
* "password": "governingdynamics
|
* "password": "governingdynamics
|
||||||
* }
|
* }
|
||||||
|
|
||||||
|
* 3. List Blocks - These are blocks that have a list of items
|
||||||
|
* ex:
|
||||||
|
* tags [
|
||||||
|
* regression
|
||||||
|
* smoke-test
|
||||||
|
* ]
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const grammar = ohm.grammar(`Bru {
|
const grammar = ohm.grammar(`Bru {
|
||||||
@@ -59,6 +66,13 @@ const grammar = ohm.grammar(`Bru {
|
|||||||
textline = textchar*
|
textline = textchar*
|
||||||
textchar = ~nl any
|
textchar = ~nl any
|
||||||
|
|
||||||
|
// List
|
||||||
|
listend = nl "]"
|
||||||
|
list = st* "[" listitems? listend
|
||||||
|
listitems = (~listend nl)* listitem (~listend stnl* listitem)* (~listend space)*
|
||||||
|
listitem = st* textchar+ st*
|
||||||
|
|
||||||
|
tags = "tags" list
|
||||||
meta = "meta" dictionary
|
meta = "meta" dictionary
|
||||||
settings = "settings" dictionary
|
settings = "settings" dictionary
|
||||||
|
|
||||||
@@ -298,6 +312,15 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
|||||||
assertkey(chars) {
|
assertkey(chars) {
|
||||||
return chars.sourceString ? chars.sourceString.trim() : '';
|
return chars.sourceString ? chars.sourceString.trim() : '';
|
||||||
},
|
},
|
||||||
|
list(_1, _2, listitems, _3) {
|
||||||
|
return listitems.ast.flat()
|
||||||
|
},
|
||||||
|
listitems(_1, listitem, _2, rest, _3) {
|
||||||
|
return [listitem.ast, ...rest.ast]
|
||||||
|
},
|
||||||
|
listitem(_1, textchar, _2) {
|
||||||
|
return textchar.sourceString;
|
||||||
|
},
|
||||||
textblock(line, _1, rest) {
|
textblock(line, _1, rest) {
|
||||||
return [line.ast, ...rest.ast].join('\n');
|
return [line.ast, ...rest.ast].join('\n');
|
||||||
},
|
},
|
||||||
@@ -319,6 +342,9 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
|||||||
_iter(...elements) {
|
_iter(...elements) {
|
||||||
return elements.map((e) => e.ast);
|
return elements.map((e) => e.ast);
|
||||||
},
|
},
|
||||||
|
tags(_1, list) {
|
||||||
|
return { tags: list.ast };
|
||||||
|
},
|
||||||
meta(_1, dictionary) {
|
meta(_1, dictionary) {
|
||||||
let meta = mapPairListToKeyValPair(dictionary.ast);
|
let meta = mapPairListToKeyValPair(dictionary.ast);
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,14 @@ const jsonToBru = (json) => {
|
|||||||
bru += '}\n\n';
|
bru += '}\n\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tags) {
|
||||||
|
bru += 'tags [\n';
|
||||||
|
for (const tag of tags) {
|
||||||
|
bru += ` ${tag}\n`;
|
||||||
|
}
|
||||||
|
bru += ']\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
if (http && http.method) {
|
if (http && http.method) {
|
||||||
bru += `${http.method} {
|
bru += `${http.method} {
|
||||||
url: ${http.url}`;
|
url: ${http.url}`;
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ meta {
|
|||||||
seq: 1
|
seq: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tags [
|
||||||
|
foo
|
||||||
|
bar
|
||||||
|
]
|
||||||
|
|
||||||
get {
|
get {
|
||||||
url: https://api.textlocal.in/send/:id
|
url: https://api.textlocal.in/send/:id
|
||||||
body: json
|
body: json
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"type": "http",
|
"type": "http",
|
||||||
"seq": "1"
|
"seq": "1"
|
||||||
},
|
},
|
||||||
|
"tags": ["foo", "bar"],
|
||||||
"http": {
|
"http": {
|
||||||
"method": "get",
|
"method": "get",
|
||||||
"url": "https://api.textlocal.in/send/:id",
|
"url": "https://api.textlocal.in/send/:id",
|
||||||
|
|||||||
@@ -310,7 +310,8 @@ const requestSchema = Yup.object({
|
|||||||
.nullable(),
|
.nullable(),
|
||||||
assertions: Yup.array().of(keyValueSchema).nullable(),
|
assertions: Yup.array().of(keyValueSchema).nullable(),
|
||||||
tests: Yup.string().nullable(),
|
tests: Yup.string().nullable(),
|
||||||
docs: Yup.string().nullable()
|
docs: Yup.string().nullable(),
|
||||||
|
tags: Yup.array().of(Yup.string().matches(/^[\w-]+$/, 'tag must be alphanumeric'))
|
||||||
})
|
})
|
||||||
.noUnknown(true)
|
.noUnknown(true)
|
||||||
.strict();
|
.strict();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ describe('Request Schema Validation', () => {
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: [],
|
headers: [],
|
||||||
params: [],
|
params: [],
|
||||||
|
tags: ['smoke-test'],
|
||||||
body: {
|
body: {
|
||||||
mode: 'none'
|
mode: 'none'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ const jsonToToml = (json) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (json.tags && json.tags.length) {
|
||||||
|
formattedJson.tags = get(json, 'tags', []);
|
||||||
|
}
|
||||||
|
|
||||||
if (json.headers && json.headers.length) {
|
if (json.headers && json.headers.length) {
|
||||||
const hasDuplicateHeaders = keyValPairHasDuplicateKeys(json.headers);
|
const hasDuplicateHeaders = keyValPairHasDuplicateKeys(json.headers);
|
||||||
const hasReservedHeaders = keyValPairHasReservedKeys(json.headers);
|
const hasReservedHeaders = keyValPairHasReservedKeys(json.headers);
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ const tomlToJson = (toml) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (json.tags && json.tags.length) {
|
||||||
|
formattedJson.tags = get(json, 'tags', []);
|
||||||
|
}
|
||||||
|
|
||||||
if (json.headers) {
|
if (json.headers) {
|
||||||
formattedJson.headers = [];
|
formattedJson.headers = [];
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"type": "http",
|
"type": "http",
|
||||||
"seq": 1
|
"seq": 1
|
||||||
},
|
},
|
||||||
|
"tags": ["foo", "bar"],
|
||||||
"http": {
|
"http": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"url": "https://reqres.in/api/users"
|
"url": "https://reqres.in/api/users"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
tags = [ 'foo', 'bar' ]
|
||||||
|
|
||||||
[meta]
|
[meta]
|
||||||
name = 'Get users'
|
name = 'Get users'
|
||||||
type = 'http'
|
type = 'http'
|
||||||
|
|||||||
Reference in New Issue
Block a user