mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-24 21:25:45 +00:00
Add view for adding and deleting request tags
This commit is contained in:
@@ -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 <Documentation item={item} collection={collection} />;
|
||||
}
|
||||
case 'tags': {
|
||||
return <Tags item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
@@ -170,6 +174,9 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
Docs
|
||||
{docs && docs.length > 0 && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('tags')} role="tab" onClick={() => selectTab('tags')}>
|
||||
Tags
|
||||
</div>
|
||||
{focusedTab.requestPaneTab === 'body' ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<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;
|
||||
39
packages/bruno-app/src/components/RequestPane/Tags/index.js
Normal file
39
packages/bruno-app/src/components/RequestPane/Tags/index.js
Normal file
@@ -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 <TagList tags={tags} onTagRemove={handleRemove} onTagAdd={handleAdd} />;
|
||||
};
|
||||
|
||||
export default Tags;
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -9,6 +9,7 @@ describe('Request Schema Validation', () => {
|
||||
method: 'GET',
|
||||
headers: [],
|
||||
params: [],
|
||||
tags: ['smoke-test'],
|
||||
body: {
|
||||
mode: 'none'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user