Add view for adding and deleting request tags

This commit is contained in:
Antti Sonkeri
2024-07-27 22:55:59 +03:00
parent 570be81467
commit 508c7018c6
9 changed files with 189 additions and 7 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ describe('Request Schema Validation', () => {
method: 'GET',
headers: [],
params: [],
tags: ['smoke-test'],
body: {
mode: 'none'
}