From 508c7018c67d29e35c7ca93a1c37dda54b876ee2 Mon Sep 17 00:00:00 2001 From: Antti Sonkeri Date: Sat, 27 Jul 2024 22:55:59 +0300 Subject: [PATCH] Add view for adding and deleting request tags --- .../RequestPane/HttpRequestPane/index.js | 7 ++ .../RequestPane/Tags/TagList/StyledWrapper.js | 25 +++++++ .../RequestPane/Tags/TagList/TagList.js | 69 +++++++++++++++++++ .../src/components/RequestPane/Tags/index.js | 39 +++++++++++ .../ReduxStore/slices/collections/index.js | 40 ++++++++++- .../bruno-app/src/utils/collections/index.js | 3 +- packages/bruno-electron/src/bru/index.js | 9 ++- .../bruno-schema/src/collections/index.js | 3 +- .../src/collections/requestSchema.spec.js | 1 + 9 files changed, 189 insertions(+), 7 deletions(-) create mode 100644 packages/bruno-app/src/components/RequestPane/Tags/TagList/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/RequestPane/Tags/TagList/TagList.js create mode 100644 packages/bruno-app/src/components/RequestPane/Tags/index.js 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
404 | Not found
; } @@ -170,6 +174,9 @@ const HttpRequestPane = ({ item, collection }) => { Docs {docs && docs.length > 0 && } +
selectTab('tags')}> + Tags +
{focusedTab.requestPaneTab === 'body' ? (
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' }