diff --git a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js
index 126613504..3bbe682e6 100644
--- a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js
+++ b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js
@@ -17,6 +17,7 @@ import { find, get } from 'lodash';
import Documentation from 'components/Documentation/index';
import HeightBoundContainer from 'ui/HeightBoundContainer';
import { useEffect } from 'react';
+import Tags from 'components/RequestPane/Tags/index';
const ContentIndicator = () => {
return (
@@ -77,6 +78,9 @@ const HttpRequestPane = ({ item, collection }) => {
case 'docs': {
return ;
}
+ case 'tags': {
+ return ;
+ }
default: {
return
diff --git a/packages/bruno-app/src/components/RequestPane/Tags/TagList/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Tags/TagList/StyledWrapper.js
new file mode 100644
index 000000000..651fc761b
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestPane/Tags/TagList/StyledWrapper.js
@@ -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;
diff --git a/packages/bruno-app/src/components/RequestPane/Tags/TagList/TagList.js b/packages/bruno-app/src/components/RequestPane/Tags/TagList/TagList.js
new file mode 100644
index 000000000..7248d54da
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestPane/Tags/TagList/TagList.js
@@ -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 (
+
+
+ {tags && tags.length
+ ? tags.map((_tag) => (
+ -
+ {_tag}
+
+
+ ))
+ : null}
+
+ {isEditing ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export default TagList;
diff --git a/packages/bruno-app/src/components/RequestPane/Tags/index.js b/packages/bruno-app/src/components/RequestPane/Tags/index.js
new file mode 100644
index 000000000..ab6763aae
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestPane/Tags/index.js
@@ -0,0 +1,39 @@
+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
;
+};
+
+export default Tags;
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
index df1fc63bc..db29f7bc0 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -2235,9 +2235,43 @@ export const collectionsSlice = createSlice({
set(folder, 'root.request.auth', {});
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 {
@@ -2350,6 +2384,8 @@ export const {
collectionGetOauth2CredentialsByUrl,
updateFolderAuth,
updateFolderAuthMode,
+ addRequestTag,
+ deleteRequestTag
} = collectionsSlice.actions;
export default collectionsSlice.reducer;
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index 5b0d28026..0509d4d38 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -542,7 +542,8 @@ export const transformRequestToSaveToFilesystem = (item) => {
vars: _item.request.vars,
assertions: _item.request.assertions,
tests: _item.request.tests,
- docs: _item.request.docs
+ docs: _item.request.docs,
+ tags: _item.request.tags
}
};
diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js
index 946d95519..9fdaf36c2 100644
--- a/packages/bruno-electron/src/bru/index.js
+++ b/packages/bruno-electron/src/bru/index.js
@@ -141,7 +141,8 @@ const bruToJson = (data, parsed = false) => {
vars: _.get(json, 'vars', {}),
assertions: _.get(json, 'assertions', []),
tests: _.get(json, 'tests', ''),
- docs: _.get(json, 'docs', '')
+ docs: _.get(json, 'docs', ''),
+ tags: _.get(json, 'tags', [])
}
};
@@ -206,7 +207,8 @@ const jsonToBru = async (json) => {
},
assertions: _.get(json, 'request.assertions', []),
tests: _.get(json, 'request.tests', ''),
- docs: _.get(json, 'request.docs', '')
+ docs: _.get(json, 'request.docs', ''),
+ tags: _.get(json, 'request.tags', [])
};
const bru = jsonToBruV2(bruJson);
@@ -247,7 +249,8 @@ const jsonToBruViaWorker = async (json) => {
},
assertions: _.get(json, 'request.assertions', []),
tests: _.get(json, 'request.tests', ''),
- docs: _.get(json, 'request.docs', '')
+ docs: _.get(json, 'request.docs', ''),
+ tags: _.get(json, 'request.tags', [])
};
const bru = await bruParserWorker?.jsonToBru(bruJson)
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index af4b13434..092f637a2 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -310,7 +310,8 @@ const requestSchema = Yup.object({
.nullable(),
assertions: Yup.array().of(keyValueSchema).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)
.strict();
diff --git a/packages/bruno-schema/src/collections/requestSchema.spec.js b/packages/bruno-schema/src/collections/requestSchema.spec.js
index 9fd223cb2..3e17e9190 100644
--- a/packages/bruno-schema/src/collections/requestSchema.spec.js
+++ b/packages/bruno-schema/src/collections/requestSchema.spec.js
@@ -9,6 +9,7 @@ describe('Request Schema Validation', () => {
method: 'GET',
headers: [],
params: [],
+ tags: ['smoke-test'],
body: {
mode: 'none'
}