mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-07 22:18:33 +00:00
collection runner tag updates
This commit is contained in:
@@ -187,17 +187,17 @@ export default class CodeEditor extends React.Component {
|
||||
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
editor.on('change', this._onEdit);
|
||||
this.addOverlay();
|
||||
|
||||
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
// Setup AutoComplete Helper for all modes
|
||||
const autoCompleteOptions = {
|
||||
showHintsFor: this.props.showHintsFor
|
||||
showHintsFor: this.props.showHintsFor,
|
||||
getAllVariables: getAllVariablesHandler
|
||||
};
|
||||
|
||||
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
||||
editor,
|
||||
getVariables,
|
||||
autoCompleteOptions
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,18 +74,20 @@ class MultiLineEditor extends Component {
|
||||
'Shift-Tab': false
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
|
||||
const getAnywordAutocompleteHints = () => this.props.autocomplete || [];
|
||||
|
||||
// Setup AutoComplete Helper
|
||||
const autoCompleteOptions = {
|
||||
showHintsFor: ['variables'],
|
||||
anywordAutocompleteHints: this.props.autocomplete
|
||||
getAllVariables: getAllVariablesHandler,
|
||||
getAnywordAutocompleteHints
|
||||
};
|
||||
|
||||
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
||||
this.editor,
|
||||
getVariables,
|
||||
autoCompleteOptions
|
||||
);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import GraphQLSchemaActions from '../GraphQLSchemaActions/index';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import Tags from 'components/RequestPane/Tags/index';
|
||||
import Settings from 'components/RequestPane/Settings';
|
||||
|
||||
const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -102,8 +102,8 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
|
||||
case 'docs': {
|
||||
return <Documentation item={item} collection={collection} />;
|
||||
}
|
||||
case 'tags': {
|
||||
return <Tags item={item} collection={collection} />;
|
||||
case 'settings': {
|
||||
return <Settings item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
@@ -156,8 +156,8 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
<div className={getTabClassname('tags')} role="tab" onClick={() => selectTab('tags')}>
|
||||
Tags
|
||||
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
|
||||
Settings
|
||||
</div>
|
||||
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,6 @@ import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import { useEffect } from 'react';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
import Settings from 'components/RequestPane/Settings';
|
||||
import Tags from 'components/RequestPane/Tags/index';
|
||||
|
||||
const HttpRequestPane = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -66,9 +65,6 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
case 'settings': {
|
||||
return <Settings item={item} collection={collection} />;
|
||||
}
|
||||
case 'tags': {
|
||||
return <Tags item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
@@ -105,6 +101,7 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
const requestVars = getPropertyFromDraftOrRequest('request.vars.req');
|
||||
const responseVars = getPropertyFromDraftOrRequest('request.vars.res');
|
||||
const auth = getPropertyFromDraftOrRequest('request.auth');
|
||||
const tags = getPropertyFromDraftOrRequest('tags');
|
||||
|
||||
const activeParamsLength = params.filter((param) => param.enabled).length;
|
||||
const activeHeadersLength = headers.filter((header) => header.enabled).length;
|
||||
@@ -168,9 +165,7 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
|
||||
Settings
|
||||
</div>
|
||||
<div className={getTabClassname('tags')} role="tab" onClick={() => selectTab('tags')}>
|
||||
Tags
|
||||
{tags && tags.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
{focusedTab.requestPaneTab === 'body' ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addRequestTag, deleteRequestTag, updateCollectionTagsList } from 'providers/ReduxStore/slices/collections';
|
||||
import TagList from 'components/TagList/index';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
const Tags = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
// all tags in the collection
|
||||
const collectionTags = collection.allTags || [];
|
||||
|
||||
// tags for the current request
|
||||
const tags = item.draft ? get(item, 'draft.tags', []) : get(item, 'tags', []);
|
||||
|
||||
// Filter out tags that are already associated with the current request
|
||||
const collectionTagsWithoutCurrentRequestTags = collectionTags?.filter(tag => !tags.includes(tag)) || [];
|
||||
|
||||
const handleAdd = useCallback((tag) => {
|
||||
const trimmedTag = tag.trim();
|
||||
if (trimmedTag && !tags.includes(trimmedTag)) {
|
||||
dispatch(
|
||||
addRequestTag({
|
||||
tag: trimmedTag,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [dispatch, tags, item.uid, collection.uid]);
|
||||
|
||||
const handleRemove = useCallback((tag) => {
|
||||
dispatch(
|
||||
deleteRequestTag({
|
||||
tag,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
}, [dispatch, item.uid, collection.uid]);
|
||||
|
||||
const handleRequestSave = () => {
|
||||
dispatch(saveRequest(item.uid, collection.uid));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(updateCollectionTagsList({ collectionUid: collection.uid }));
|
||||
}, [collection.uid, dispatch]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<TagList
|
||||
tagsHintList={collectionTagsWithoutCurrentRequestTags}
|
||||
handleAddTag={handleAdd}
|
||||
handleRemoveTag={handleRemove}
|
||||
tags={tags}
|
||||
onSave={handleRequestSave}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tags;
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import get from 'lodash/get';
|
||||
import { IconTag } from '@tabler/icons';
|
||||
import ToggleSelector from 'components/RequestPane/Settings/ToggleSelector';
|
||||
import { updateItemSettings } from 'providers/ReduxStore/slices/collections';
|
||||
import Tags from './Tags/index';
|
||||
|
||||
const Settings = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -22,7 +24,16 @@ const Settings = ({ item, collection }) => {
|
||||
}, [encodeUrl, dispatch, collection.uid, item.uid]);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-2">
|
||||
<div className="w-full h-full flex flex-col gap-10">
|
||||
<div className='flex flex-col gap-2 max-w-[400px]'>
|
||||
<h3 className="text-xs font-medium text-gray-900 dark:text-gray-100 flex items-center gap-1">
|
||||
<IconTag size={16} />
|
||||
Tags
|
||||
</h3>
|
||||
<div label="Tags">
|
||||
<Tags item={item} collection={collection} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<ToggleSelector
|
||||
checked={encodeUrl}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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;
|
||||
@@ -1,69 +0,0 @@
|
||||
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;
|
||||
@@ -1,43 +0,0 @@
|
||||
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;
|
||||
@@ -0,0 +1,128 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { get, cloneDeep, find } from 'lodash';
|
||||
import { updateCollectionTagsList, updateRunnerTagsDetails } from 'providers/ReduxStore/slices/collections';
|
||||
import TagList from 'components/TagList';
|
||||
|
||||
const RunnerTags = ({ collectionUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const collection = cloneDeep(find(collections, (c) => c.uid === collectionUid));
|
||||
|
||||
// tags for the collection run
|
||||
const tags = get(collection, 'runnerTags', { include: [], exclude: [] });
|
||||
|
||||
// have tags been enabled for the collection run
|
||||
const tagsEnabled = get(collection, 'runnerTagsEnabled', false);
|
||||
|
||||
// all available tags in the collection that can be used for filtering
|
||||
const availableTags = get(collection, 'allTags', []);
|
||||
const tagsHintList = availableTags.filter(t => !tags.exclude.includes(t) && !tags.include.includes(t));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(updateCollectionTagsList({ collectionUid }));
|
||||
}, [collection.uid, dispatch]);
|
||||
|
||||
const handleValidation = (tag) => {
|
||||
const trimmedTag = tag.trim();
|
||||
if (!availableTags.includes(trimmedTag)) {
|
||||
return 'tag does not exist!';
|
||||
}
|
||||
if (tags.include.includes(trimmedTag)) {
|
||||
return 'tag already present in the include list!';
|
||||
}
|
||||
if (tags.exclude.includes(trimmedTag)) {
|
||||
return 'tag is present in the exclude list!';
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddTag = ({ tag, to }) => {
|
||||
const trimmedTag = tag.trim();
|
||||
if (!trimmedTag) return;
|
||||
// add tag to the `include` list
|
||||
if (to === 'include') {
|
||||
if (tags.include.includes(trimmedTag) || tags.exclude.includes(trimmedTag)) return;
|
||||
if (!availableTags.includes(trimmedTag)) {
|
||||
return;
|
||||
}
|
||||
const newTags = { ...tags, include: [...tags.include, trimmedTag].sort() };
|
||||
setTags(newTags);
|
||||
return;
|
||||
}
|
||||
// add tag to the `exclude` list
|
||||
if (to === 'exclude') {
|
||||
if (tags.include.includes(trimmedTag) || tags.exclude.includes(trimmedTag)) return;
|
||||
if (!availableTags.includes(trimmedTag)) {
|
||||
return;
|
||||
}
|
||||
const newTags = { ...tags, exclude: [...tags.exclude, trimmedTag].sort() };
|
||||
setTags(newTags);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTag = ({ tag, from }) => {
|
||||
const trimmedTag = tag.trim();
|
||||
if (!trimmedTag) return;
|
||||
// remove tag from the `include` list
|
||||
if (from === 'include') {
|
||||
if (!tags.include.includes(trimmedTag)) return;
|
||||
const newTags = { ...tags, include: tags.include.filter((t) => t !== trimmedTag) };
|
||||
setTags(newTags);
|
||||
return;
|
||||
}
|
||||
// remove tag from the `exclude` list
|
||||
if (from === 'exclude') {
|
||||
if (!tags.exclude.includes(trimmedTag)) return;
|
||||
const newTags = { ...tags, exclude: tags.exclude.filter((t) => t !== trimmedTag) };
|
||||
setTags(newTags);
|
||||
}
|
||||
};
|
||||
|
||||
const setTags = (tags) => {
|
||||
dispatch(updateRunnerTagsDetails({ collectionUid: collection.uid, tags }));
|
||||
};
|
||||
|
||||
const setTagsEnabled = (tagsEnabled) => {
|
||||
dispatch(updateRunnerTagsDetails({ collectionUid: collection.uid, tagsEnabled }));
|
||||
};
|
||||
|
||||
return (
|
||||
<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 flex-row mt-4 gap-4 w-full">
|
||||
<div className="w-1/2 flex flex-col gap-2 max-w-[400px]">
|
||||
<span>Included tags:</span>
|
||||
<TagList
|
||||
tags={tags.include}
|
||||
handleAddTag={tag => handleAddTag({ tag, to: 'include' })}
|
||||
handleRemoveTag={tag => handleRemoveTag({ tag, from: 'include' })}
|
||||
tagsHintList={tagsHintList}
|
||||
handleValidation={handleValidation}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2 flex flex-col gap-2 max-w-[400px]">
|
||||
<span>Excluded tags:</span>
|
||||
<TagList
|
||||
tags={tags.exclude}
|
||||
handleAddTag={tag => handleAddTag({ tag, to: 'exclude' })}
|
||||
handleRemoveTag={tag => handleRemoveTag({ tag, from: 'exclude' })}
|
||||
tagsHintList={tagsHintList}
|
||||
handleValidation={handleValidation}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RunnerTags;
|
||||
@@ -9,7 +9,7 @@ import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, Ic
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
import TagList from 'components/RequestPane/Tags/TagList/TagList';
|
||||
import RunnerTags from './RunnerTags/index';
|
||||
|
||||
const getDisplayName = (fullPath, pathname, name = '') => {
|
||||
let relativePath = path.relative(fullPath, pathname);
|
||||
@@ -43,8 +43,6 @@ export default function RunnerResults({ collection }) {
|
||||
const dispatch = useDispatch();
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [delay, setDelay] = useState(null);
|
||||
const [tags, setTags] = useState({ include: [], exclude: [] });
|
||||
const [tagsEnabled, setTagsEnabled] = useState(false);
|
||||
|
||||
// ref for the runner output body
|
||||
const runnerBodyRef = useRef();
|
||||
@@ -66,6 +64,15 @@ export default function RunnerResults({ collection }) {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
|
||||
// tags for the collection run
|
||||
const tags = get(collection, 'runnerTags', { include: [], exclude: [] });
|
||||
|
||||
// have tags been enabled for the collection run
|
||||
const tagsEnabled = get(collection, 'runnerTagsEnabled', false);
|
||||
|
||||
// have tags been added for the collection run
|
||||
const areTagsAdded = tags.include.length > 0 || tags.exclude.length > 0;
|
||||
|
||||
const items = cloneDeep(get(collection, 'runnerResult.items', []))
|
||||
.map((item) => {
|
||||
const info = findItemInCollection(collectionCopy, item.uid);
|
||||
@@ -78,7 +85,8 @@ export default function RunnerResults({ collection }) {
|
||||
type: info.type,
|
||||
filename: info.filename,
|
||||
pathname: info.pathname,
|
||||
displayName: getDisplayName(collection.pathname, info.pathname, info.name)
|
||||
displayName: getDisplayName(collection.pathname, info.pathname, info.name),
|
||||
tags: [...(info.request?.tags || [])].sort(),
|
||||
};
|
||||
if (newItem.status !== 'error' && newItem.status !== 'skipped') {
|
||||
newItem.testStatus = getTestStatus(newItem.testResults);
|
||||
@@ -151,37 +159,9 @@ export default function RunnerResults({ collection }) {
|
||||
onChange={(e) => setDelay(e.target.value)}
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* Tags for the collection run */}
|
||||
<RunnerTags collectionUid={collection.uid} />
|
||||
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runCollection}>
|
||||
Run Collection
|
||||
@@ -216,11 +196,25 @@ export default function RunnerResults({ collection }) {
|
||||
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}, Skipped:{' '}
|
||||
{skippedRequests.length}
|
||||
</div>
|
||||
{tagsEnabled && areTagsAdded && (
|
||||
<div className="pb-2 text-xs flex flex-row gap-1">
|
||||
Tags:
|
||||
<div className='flex flex-row items-center gap-x-2'>
|
||||
<div className="text-green-500">
|
||||
{tags.include.join(', ')}
|
||||
</div>
|
||||
<div className="text-gray-500">
|
||||
{tags.exclude.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{runnerInfo?.statusText ?
|
||||
<div className="pb-2 font-medium danger">
|
||||
{runnerInfo?.statusText}
|
||||
</div>
|
||||
: null}
|
||||
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<div key={item.uid}>
|
||||
@@ -256,6 +250,11 @@ export default function RunnerResults({ collection }) {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{tagsEnabled && areTagsAdded && item?.tags?.length > 0 && (
|
||||
<div className="pl-7 text-xs text-gray-500">
|
||||
Tags: {item.tags.filter(t => tags.include.includes(t)).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{item.status == 'error' ? <div className="error-message pl-8 pt-2 text-xs">{item.error}</div> : null}
|
||||
|
||||
<ul className="pl-8">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { uuid } from 'utils/common';
|
||||
import Modal from 'components/Modal';
|
||||
@@ -8,15 +8,19 @@ import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/act
|
||||
import { flattenItems } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
import TagList from 'components/RequestPane/Tags/TagList/TagList';
|
||||
import RunnerTags from 'components/RunnerResults/RunnerTags/index';
|
||||
|
||||
const RunCollectionItem = ({ collectionUid, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
|
||||
const isCollectionRunInProgress = collection?.runnerResult?.info?.status && (collection?.runnerResult?.info?.status !== 'ended');
|
||||
const [tags, setTags] = useState({ include: [], exclude: [] });
|
||||
const [tagsEnabled, setTagsEnabled] = useState(false);
|
||||
|
||||
// tags for the collection run
|
||||
const tags = get(collection, 'runnerTags', { include: [], exclude: [] });
|
||||
|
||||
// have tags been enabled for the collection run
|
||||
const tagsEnabled = get(collection, 'runnerTagsEnabled', false);
|
||||
|
||||
const onSubmit = (recursive) => {
|
||||
dispatch(
|
||||
@@ -75,37 +79,8 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => {
|
||||
{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}
|
||||
|
||||
<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>
|
||||
{/* Tags for the collection run */}
|
||||
<RunnerTags collectionUid={collection.uid} />
|
||||
|
||||
<div className="flex justify-end bruno-modal-footer">
|
||||
<span className="mr-3">
|
||||
|
||||
@@ -74,17 +74,19 @@ class SingleLineEditor extends Component {
|
||||
}
|
||||
});
|
||||
|
||||
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
|
||||
const getAnywordAutocompleteHints = () => this.props.autocomplete || [];
|
||||
|
||||
// Setup AutoComplete Helper
|
||||
const autoCompleteOptions = {
|
||||
showHintsFor: ['variables'],
|
||||
anywordAutocompleteHints: this.props.autocomplete
|
||||
getAllVariables: getAllVariablesHandler,
|
||||
getAnywordAutocompleteHints,
|
||||
showHintsFor: this.props.showHintsFor || ['variables'],
|
||||
showHintsOnClick: this.props.showHintsOnClick
|
||||
};
|
||||
|
||||
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
||||
this.editor,
|
||||
getVariables,
|
||||
autoCompleteOptions
|
||||
);
|
||||
|
||||
@@ -189,7 +191,7 @@ class SingleLineEditor extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="flex flex-row justify-between w-full overflow-x-auto">
|
||||
<div className={`flex flex-row justify-between w-full overflow-x-auto ${this.props.className}`}>
|
||||
<StyledWrapper ref={this.editorRef} className="single-line-editor grow" />
|
||||
{this.secretEye(this.props.isSecret)}
|
||||
</div>
|
||||
|
||||
132
packages/bruno-app/src/components/TagList/StyledWrapper.js
Normal file
132
packages/bruno-app/src/components/TagList/StyledWrapper.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-height: 40px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 3px 7px;
|
||||
background-color: ${(props) => props.theme.sidebar.bg};
|
||||
border: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.text};
|
||||
max-width: 200px;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.requestTabs.active.bg};
|
||||
border-color: ${(props) => props.theme.requestTabs.active.border || props.theme.requestTabs.bottomBorder};
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.tag-icon {
|
||||
color: ${(props) => props.theme.textSecondary || props.theme.text};
|
||||
opacity: 0.7;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tag-remove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
color: ${(props) => props.theme.textSecondary || props.theme.text};
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.danger};
|
||||
color: white;
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid ${(props) => props.theme.danger};
|
||||
outline-offset: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 24px 16px;
|
||||
background-color: ${(props) => props.theme.sidebar.bg};
|
||||
border: 2px dashed ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
border-radius: 3px;
|
||||
color: ${(props) => props.theme.textSecondary || props.theme.text};
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
opacity: 0.5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-weight: 600;
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 14px;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
.empty-subtitle {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
line-height: 1.5;
|
||||
color: ${(props) => props.theme.textSecondary || props.theme.text};
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 480px) {
|
||||
.tags-container {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 16px 12px;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
77
packages/bruno-app/src/components/TagList/index.js
Normal file
77
packages/bruno-app/src/components/TagList/index.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useState } from 'react';
|
||||
import { IconX, IconTag } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import SingleLineEditor from 'components/SingleLineEditor/index';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
|
||||
const TagList = ({ tagsHintList = [], handleAddTag, tags, handleRemoveTag, onSave, handleValidation }) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
const tagNameRegex = /^[\w-]+$/;
|
||||
const [text, setText] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleInputChange = (value) => {
|
||||
setError('');
|
||||
setText(value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (!tagNameRegex.test(text)) {
|
||||
setError('Tags must only contain alpha-numeric characters, "-", "_"');
|
||||
return;
|
||||
}
|
||||
if (tags.includes(text)) {
|
||||
setError(`Tag "${text}" already exists`);
|
||||
return;
|
||||
}
|
||||
if (handleValidation) {
|
||||
const error = handleValidation(text);
|
||||
if (error) {
|
||||
setError(error);
|
||||
setText('');
|
||||
return;
|
||||
}
|
||||
}
|
||||
handleAddTag(text);
|
||||
setText('');
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-wrap flex-col gap-2">
|
||||
<SingleLineEditor
|
||||
className="border border-gray-500/50 px-2"
|
||||
value={text}
|
||||
placeholder="Enter tag name (e.g., smoke, regression etc)"
|
||||
autocomplete={tagsHintList}
|
||||
showHintsOnClick={true}
|
||||
showHintsFor={[]}
|
||||
theme={displayedTheme}
|
||||
onChange={handleInputChange}
|
||||
onRun={handleKeyDown}
|
||||
onSave={onSave}
|
||||
/>
|
||||
{error && <span className='text-xs text-red-500'>{error}</span>}
|
||||
<ul className="flex flex-wrap gap-1">
|
||||
{tags && tags.length
|
||||
? tags.map((_tag) => (
|
||||
<li key={_tag}>
|
||||
<button
|
||||
className="tag-item"
|
||||
onClick={() => handleRemoveTag(_tag)}
|
||||
type="button"
|
||||
>
|
||||
<IconTag size={12} className="tag-icon" aria-hidden="true" />
|
||||
<span className="tag-text" title={_tag}>
|
||||
{_tag}
|
||||
</span>
|
||||
<IconX size={12} strokeWidth={2} aria-hidden="true" />
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
</ul>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagList;
|
||||
Reference in New Issue
Block a user