mirror of
https://github.com/usebruno/bruno.git
synced 2025-08-18 20:55:10 +02:00
collection runner tag updates
This commit is contained in:
5
package-lock.json
generated
5
package-lock.json
generated
@@ -32959,9 +32959,14 @@
|
|||||||
"axios": "^1.9.0"
|
"axios": "^1.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/preset-env": "^7.22.0",
|
||||||
|
"@babel/preset-typescript": "^7.22.0",
|
||||||
"@rollup/plugin-commonjs": "^23.0.2",
|
"@rollup/plugin-commonjs": "^23.0.2",
|
||||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||||
"@rollup/plugin-typescript": "^9.0.2",
|
"@rollup/plugin-typescript": "^9.0.2",
|
||||||
|
"@types/jest": "^29.5.11",
|
||||||
|
"babel-jest": "^29.7.0",
|
||||||
|
"jest": "^29.2.0",
|
||||||
"rollup": "3.29.5",
|
"rollup": "3.29.5",
|
||||||
"rollup-plugin-dts": "^5.0.0",
|
"rollup-plugin-dts": "^5.0.0",
|
||||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||||
|
@@ -188,16 +188,16 @@ export default class CodeEditor extends React.Component {
|
|||||||
editor.on('change', this._onEdit);
|
editor.on('change', this._onEdit);
|
||||||
this.addOverlay();
|
this.addOverlay();
|
||||||
|
|
||||||
|
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
|
||||||
|
|
||||||
// Setup AutoComplete Helper for all modes
|
// Setup AutoComplete Helper for all modes
|
||||||
const autoCompleteOptions = {
|
const autoCompleteOptions = {
|
||||||
showHintsFor: this.props.showHintsFor
|
showHintsFor: this.props.showHintsFor,
|
||||||
|
getAllVariables: getAllVariablesHandler
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
|
|
||||||
|
|
||||||
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
||||||
editor,
|
editor,
|
||||||
getVariables,
|
|
||||||
autoCompleteOptions
|
autoCompleteOptions
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -75,17 +75,19 @@ class MultiLineEditor extends Component {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
|
||||||
|
const getAnywordAutocompleteHints = () => this.props.autocomplete || [];
|
||||||
|
|
||||||
// Setup AutoComplete Helper
|
// Setup AutoComplete Helper
|
||||||
const autoCompleteOptions = {
|
const autoCompleteOptions = {
|
||||||
showHintsFor: ['variables'],
|
showHintsFor: ['variables'],
|
||||||
anywordAutocompleteHints: this.props.autocomplete
|
getAllVariables: getAllVariablesHandler,
|
||||||
|
getAnywordAutocompleteHints
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
|
|
||||||
|
|
||||||
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
||||||
this.editor,
|
this.editor,
|
||||||
getVariables,
|
|
||||||
autoCompleteOptions
|
autoCompleteOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -19,7 +19,7 @@ import StyledWrapper from './StyledWrapper';
|
|||||||
import Documentation from 'components/Documentation/index';
|
import Documentation from 'components/Documentation/index';
|
||||||
import GraphQLSchemaActions from '../GraphQLSchemaActions/index';
|
import GraphQLSchemaActions from '../GraphQLSchemaActions/index';
|
||||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
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 GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -102,8 +102,8 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
|
|||||||
case 'docs': {
|
case 'docs': {
|
||||||
return <Documentation item={item} collection={collection} />;
|
return <Documentation item={item} collection={collection} />;
|
||||||
}
|
}
|
||||||
case 'tags': {
|
case 'settings': {
|
||||||
return <Tags item={item} collection={collection} />;
|
return <Settings item={item} collection={collection} />;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return <div className="mt-4">404 | Not found</div>;
|
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')}>
|
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||||
Docs
|
Docs
|
||||||
</div>
|
</div>
|
||||||
<div className={getTabClassname('tags')} role="tab" onClick={() => selectTab('tags')}>
|
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
|
||||||
Tags
|
Settings
|
||||||
</div>
|
</div>
|
||||||
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
||||||
</div>
|
</div>
|
||||||
|
@@ -18,7 +18,6 @@ import HeightBoundContainer from 'ui/HeightBoundContainer';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import StatusDot from 'components/StatusDot';
|
import StatusDot from 'components/StatusDot';
|
||||||
import Settings from 'components/RequestPane/Settings';
|
import Settings from 'components/RequestPane/Settings';
|
||||||
import Tags from 'components/RequestPane/Tags/index';
|
|
||||||
|
|
||||||
const HttpRequestPane = ({ item, collection }) => {
|
const HttpRequestPane = ({ item, collection }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -66,9 +65,6 @@ const HttpRequestPane = ({ item, collection }) => {
|
|||||||
case 'settings': {
|
case 'settings': {
|
||||||
return <Settings item={item} collection={collection} />;
|
return <Settings item={item} collection={collection} />;
|
||||||
}
|
}
|
||||||
case 'tags': {
|
|
||||||
return <Tags item={item} collection={collection} />;
|
|
||||||
}
|
|
||||||
default: {
|
default: {
|
||||||
return <div className="mt-4">404 | Not found</div>;
|
return <div className="mt-4">404 | Not found</div>;
|
||||||
}
|
}
|
||||||
@@ -105,6 +101,7 @@ const HttpRequestPane = ({ item, collection }) => {
|
|||||||
const requestVars = getPropertyFromDraftOrRequest('request.vars.req');
|
const requestVars = getPropertyFromDraftOrRequest('request.vars.req');
|
||||||
const responseVars = getPropertyFromDraftOrRequest('request.vars.res');
|
const responseVars = getPropertyFromDraftOrRequest('request.vars.res');
|
||||||
const auth = getPropertyFromDraftOrRequest('request.auth');
|
const auth = getPropertyFromDraftOrRequest('request.auth');
|
||||||
|
const tags = getPropertyFromDraftOrRequest('tags');
|
||||||
|
|
||||||
const activeParamsLength = params.filter((param) => param.enabled).length;
|
const activeParamsLength = params.filter((param) => param.enabled).length;
|
||||||
const activeHeadersLength = headers.filter((header) => header.enabled).length;
|
const activeHeadersLength = headers.filter((header) => header.enabled).length;
|
||||||
@@ -168,9 +165,7 @@ const HttpRequestPane = ({ item, collection }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
|
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
|
||||||
Settings
|
Settings
|
||||||
</div>
|
{tags && tags.length > 0 && <StatusDot />}
|
||||||
<div className={getTabClassname('tags')} role="tab" onClick={() => selectTab('tags')}>
|
|
||||||
Tags
|
|
||||||
</div>
|
</div>
|
||||||
{focusedTab.requestPaneTab === 'body' ? (
|
{focusedTab.requestPaneTab === 'body' ? (
|
||||||
<div className="flex flex-grow justify-end items-center">
|
<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 { useDispatch } from 'react-redux';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
|
import { IconTag } from '@tabler/icons';
|
||||||
import ToggleSelector from 'components/RequestPane/Settings/ToggleSelector';
|
import ToggleSelector from 'components/RequestPane/Settings/ToggleSelector';
|
||||||
import { updateItemSettings } from 'providers/ReduxStore/slices/collections';
|
import { updateItemSettings } from 'providers/ReduxStore/slices/collections';
|
||||||
|
import Tags from './Tags/index';
|
||||||
|
|
||||||
const Settings = ({ item, collection }) => {
|
const Settings = ({ item, collection }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -22,7 +24,16 @@ const Settings = ({ item, collection }) => {
|
|||||||
}, [encodeUrl, dispatch, collection.uid, item.uid]);
|
}, [encodeUrl, dispatch, collection.uid, item.uid]);
|
||||||
|
|
||||||
return (
|
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'>
|
<div className='flex flex-col gap-4'>
|
||||||
<ToggleSelector
|
<ToggleSelector
|
||||||
checked={encodeUrl}
|
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 ResponsePane from './ResponsePane';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import { areItemsLoading } from 'utils/collections';
|
import { areItemsLoading } from 'utils/collections';
|
||||||
import TagList from 'components/RequestPane/Tags/TagList/TagList';
|
import RunnerTags from './RunnerTags/index';
|
||||||
|
|
||||||
const getDisplayName = (fullPath, pathname, name = '') => {
|
const getDisplayName = (fullPath, pathname, name = '') => {
|
||||||
let relativePath = path.relative(fullPath, pathname);
|
let relativePath = path.relative(fullPath, pathname);
|
||||||
@@ -43,8 +43,6 @@ export default function RunnerResults({ collection }) {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [selectedItem, setSelectedItem] = useState(null);
|
const [selectedItem, setSelectedItem] = useState(null);
|
||||||
const [delay, setDelay] = useState(null);
|
const [delay, setDelay] = useState(null);
|
||||||
const [tags, setTags] = useState({ include: [], exclude: [] });
|
|
||||||
const [tagsEnabled, setTagsEnabled] = useState(false);
|
|
||||||
|
|
||||||
// ref for the runner output body
|
// ref for the runner output body
|
||||||
const runnerBodyRef = useRef();
|
const runnerBodyRef = useRef();
|
||||||
@@ -66,6 +64,15 @@ export default function RunnerResults({ collection }) {
|
|||||||
const collectionCopy = cloneDeep(collection);
|
const collectionCopy = cloneDeep(collection);
|
||||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
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', []))
|
const items = cloneDeep(get(collection, 'runnerResult.items', []))
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const info = findItemInCollection(collectionCopy, item.uid);
|
const info = findItemInCollection(collectionCopy, item.uid);
|
||||||
@@ -78,7 +85,8 @@ export default function RunnerResults({ collection }) {
|
|||||||
type: info.type,
|
type: info.type,
|
||||||
filename: info.filename,
|
filename: info.filename,
|
||||||
pathname: info.pathname,
|
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') {
|
if (newItem.status !== 'error' && newItem.status !== 'skipped') {
|
||||||
newItem.testStatus = getTestStatus(newItem.testResults);
|
newItem.testStatus = getTestStatus(newItem.testResults);
|
||||||
@@ -151,37 +159,9 @@ export default function RunnerResults({ collection }) {
|
|||||||
onChange={(e) => setDelay(e.target.value)}
|
onChange={(e) => setDelay(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 flex flex-col">
|
|
||||||
<div className="flex gap-2">
|
{/* Tags for the collection run */}
|
||||||
<label className="block font-medium">Filter requests with tags</label>
|
<RunnerTags collectionUid={collection.uid} />
|
||||||
<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>
|
|
||||||
|
|
||||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runCollection}>
|
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runCollection}>
|
||||||
Run Collection
|
Run Collection
|
||||||
@@ -216,11 +196,25 @@ export default function RunnerResults({ collection }) {
|
|||||||
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}, Skipped:{' '}
|
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}, Skipped:{' '}
|
||||||
{skippedRequests.length}
|
{skippedRequests.length}
|
||||||
</div>
|
</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 ?
|
{runnerInfo?.statusText ?
|
||||||
<div className="pb-2 font-medium danger">
|
<div className="pb-2 font-medium danger">
|
||||||
{runnerInfo?.statusText}
|
{runnerInfo?.statusText}
|
||||||
</div>
|
</div>
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<div key={item.uid}>
|
<div key={item.uid}>
|
||||||
@@ -256,6 +250,11 @@ export default function RunnerResults({ collection }) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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}
|
{item.status == 'error' ? <div className="error-message pl-8 pt-2 text-xs">{item.error}</div> : null}
|
||||||
|
|
||||||
<ul className="pl-8">
|
<ul className="pl-8">
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { uuid } from 'utils/common';
|
import { uuid } from 'utils/common';
|
||||||
import Modal from 'components/Modal';
|
import Modal from 'components/Modal';
|
||||||
@@ -8,15 +8,19 @@ import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/act
|
|||||||
import { flattenItems } from 'utils/collections';
|
import { flattenItems } from 'utils/collections';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import { areItemsLoading } from 'utils/collections';
|
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 RunCollectionItem = ({ collectionUid, item, onClose }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
|
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
|
||||||
const isCollectionRunInProgress = collection?.runnerResult?.info?.status && (collection?.runnerResult?.info?.status !== 'ended');
|
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) => {
|
const onSubmit = (recursive) => {
|
||||||
dispatch(
|
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}
|
{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}
|
{isCollectionRunInProgress ? <div className='mb-6 warning'>A Collection Run is already in progress.</div> : null}
|
||||||
|
|
||||||
<div className="mb-8 flex flex-col">
|
{/* Tags for the collection run */}
|
||||||
<div className="flex gap-2">
|
<RunnerTags collectionUid={collection.uid} />
|
||||||
<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>
|
|
||||||
|
|
||||||
<div className="flex justify-end bruno-modal-footer">
|
<div className="flex justify-end bruno-modal-footer">
|
||||||
<span className="mr-3">
|
<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
|
// Setup AutoComplete Helper
|
||||||
const autoCompleteOptions = {
|
const autoCompleteOptions = {
|
||||||
showHintsFor: ['variables'],
|
getAllVariables: getAllVariablesHandler,
|
||||||
anywordAutocompleteHints: this.props.autocomplete
|
getAnywordAutocompleteHints,
|
||||||
|
showHintsFor: this.props.showHintsFor || ['variables'],
|
||||||
|
showHintsOnClick: this.props.showHintsOnClick
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
|
|
||||||
|
|
||||||
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
||||||
this.editor,
|
this.editor,
|
||||||
getVariables,
|
|
||||||
autoCompleteOptions
|
autoCompleteOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -189,7 +191,7 @@ class SingleLineEditor extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
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" />
|
<StyledWrapper ref={this.editorRef} className="single-line-editor grow" />
|
||||||
{this.secretEye(this.props.isSecret)}
|
{this.secretEye(this.props.isSecret)}
|
||||||
</div>
|
</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;
|
@@ -20,6 +20,7 @@ import { getSubdirectoriesFromRoot } from 'utils/common/platform';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import mime from 'mime-types';
|
import mime from 'mime-types';
|
||||||
import path from 'utils/common/path';
|
import path from 'utils/common/path';
|
||||||
|
import { getUniqueTagsFromItems } from 'utils/collections/index';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
collections: [],
|
collections: [],
|
||||||
@@ -36,6 +37,7 @@ export const collectionsSlice = createSlice({
|
|||||||
|
|
||||||
collection.settingsSelectedTab = 'overview';
|
collection.settingsSelectedTab = 'overview';
|
||||||
collection.folderLevelSettingsSelectedTab = {};
|
collection.folderLevelSettingsSelectedTab = {};
|
||||||
|
collection.allTags = []; // Initialize collection-level tags
|
||||||
|
|
||||||
// Collection mount status is used to track the mount status of the collection
|
// Collection mount status is used to track the mount status of the collection
|
||||||
// values can be 'unmounted', 'mounting', 'mounted'
|
// values can be 'unmounted', 'mounting', 'mounted'
|
||||||
@@ -1860,6 +1862,7 @@ export const collectionsSlice = createSlice({
|
|||||||
currentItem.name = file.data.name;
|
currentItem.name = file.data.name;
|
||||||
currentItem.type = file.data.type;
|
currentItem.type = file.data.type;
|
||||||
currentItem.seq = file.data.seq;
|
currentItem.seq = file.data.seq;
|
||||||
|
currentItem.tags = file.data.tags;
|
||||||
currentItem.request = file.data.request;
|
currentItem.request = file.data.request;
|
||||||
currentItem.filename = file.meta.name;
|
currentItem.filename = file.meta.name;
|
||||||
currentItem.pathname = file.meta.pathname;
|
currentItem.pathname = file.meta.pathname;
|
||||||
@@ -1875,6 +1878,7 @@ export const collectionsSlice = createSlice({
|
|||||||
name: file.data.name,
|
name: file.data.name,
|
||||||
type: file.data.type,
|
type: file.data.type,
|
||||||
seq: file.data.seq,
|
seq: file.data.seq,
|
||||||
|
tags: file.data.tags,
|
||||||
request: file.data.request,
|
request: file.data.request,
|
||||||
settings: file.data.settings,
|
settings: file.data.settings,
|
||||||
filename: file.meta.name,
|
filename: file.meta.name,
|
||||||
@@ -1965,6 +1969,7 @@ export const collectionsSlice = createSlice({
|
|||||||
item.name = file.data.name;
|
item.name = file.data.name;
|
||||||
item.type = file.data.type;
|
item.type = file.data.type;
|
||||||
item.seq = file.data.seq;
|
item.seq = file.data.seq;
|
||||||
|
item.tags = file.data.tags;
|
||||||
item.request = file.data.request;
|
item.request = file.data.request;
|
||||||
item.settings = file.data.settings;
|
item.settings = file.data.settings;
|
||||||
item.filename = file.meta.name;
|
item.filename = file.meta.name;
|
||||||
@@ -2224,6 +2229,20 @@ export const collectionsSlice = createSlice({
|
|||||||
|
|
||||||
if (collection) {
|
if (collection) {
|
||||||
collection.runnerResult = null;
|
collection.runnerResult = null;
|
||||||
|
collection.runnerTags = { include: [], exclude: [] }
|
||||||
|
collection.runnerTagsEnabled = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateRunnerTagsDetails: (state, action) => {
|
||||||
|
const { collectionUid, tags, tagsEnabled } = action.payload;
|
||||||
|
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||||
|
if (collection) {
|
||||||
|
if (tags) {
|
||||||
|
collection.runnerTags = tags;
|
||||||
|
}
|
||||||
|
if (typeof tagsEnabled === 'boolean') {
|
||||||
|
collection.runnerTagsEnabled = tagsEnabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateRequestDocs: (state, action) => {
|
updateRequestDocs: (state, action) => {
|
||||||
@@ -2352,10 +2371,12 @@ export const collectionsSlice = createSlice({
|
|||||||
if (!item.draft) {
|
if (!item.draft) {
|
||||||
item.draft = cloneDeep(item);
|
item.draft = cloneDeep(item);
|
||||||
}
|
}
|
||||||
item.draft.request.tags = item.draft.request.tags || [];
|
item.draft.tags = item.draft.tags || [];
|
||||||
if (!item.draft.request.tags.includes(tag.trim())) {
|
if (!item.draft.tags.includes(tag.trim())) {
|
||||||
item.draft.request.tags.push(tag.trim());
|
item.draft.tags.push(tag.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
collection.allTags = getUniqueTagsFromItems(collection.items);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2370,10 +2391,20 @@ export const collectionsSlice = createSlice({
|
|||||||
if (!item.draft) {
|
if (!item.draft) {
|
||||||
item.draft = cloneDeep(item);
|
item.draft = cloneDeep(item);
|
||||||
}
|
}
|
||||||
item.draft.request.tags = item.draft.request.tags || [];
|
item.draft.tags = item.draft.tags || [];
|
||||||
item.draft.request.tags = item.draft.request.tags.filter((t) => t !== tag.trim());
|
item.draft.tags = item.draft.tags.filter((t) => t !== tag.trim());
|
||||||
|
|
||||||
|
collection.allTags = getUniqueTagsFromItems(collection.items);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
updateCollectionTagsList: (state, action) => {
|
||||||
|
const { collectionUid } = action.payload;
|
||||||
|
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
collection.allTags = getUniqueTagsFromItems(collection.items);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2483,6 +2514,7 @@ export const {
|
|||||||
runRequestEvent,
|
runRequestEvent,
|
||||||
runFolderEvent,
|
runFolderEvent,
|
||||||
resetCollectionRunner,
|
resetCollectionRunner,
|
||||||
|
updateRunnerTagsDetails,
|
||||||
updateRequestDocs,
|
updateRequestDocs,
|
||||||
updateFolderDocs,
|
updateFolderDocs,
|
||||||
moveCollection,
|
moveCollection,
|
||||||
@@ -2492,7 +2524,8 @@ export const {
|
|||||||
updateFolderAuth,
|
updateFolderAuth,
|
||||||
updateFolderAuthMode,
|
updateFolderAuthMode,
|
||||||
addRequestTag,
|
addRequestTag,
|
||||||
deleteRequestTag
|
deleteRequestTag,
|
||||||
|
updateCollectionTagsList
|
||||||
} = collectionsSlice.actions;
|
} = collectionsSlice.actions;
|
||||||
|
|
||||||
export default collectionsSlice.reducer;
|
export default collectionsSlice.reducer;
|
||||||
|
@@ -178,11 +178,11 @@ const addVariableHintsToSet = (variableHints, allVariables) => {
|
|||||||
/**
|
/**
|
||||||
* Add custom hints to categorized hints
|
* Add custom hints to categorized hints
|
||||||
* @param {Set} anywordHints - Set to add custom hints to
|
* @param {Set} anywordHints - Set to add custom hints to
|
||||||
* @param {Object} options - Options containing custom hints
|
* @param {string[]} customHints - Array of custom hints
|
||||||
*/
|
*/
|
||||||
const addCustomHintsToSet = (anywordHints, options) => {
|
const addCustomHintsToSet = (anywordHints, customHints) => {
|
||||||
if (options.anywordAutocompleteHints && Array.isArray(options.anywordAutocompleteHints)) {
|
if (customHints && Array.isArray(customHints)) {
|
||||||
options.anywordAutocompleteHints.forEach(hint => {
|
customHints.forEach(hint => {
|
||||||
generateProgressiveHints(hint).forEach(h => anywordHints.add(h));
|
generateProgressiveHints(hint).forEach(h => anywordHints.add(h));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -191,10 +191,11 @@ const addCustomHintsToSet = (anywordHints, options) => {
|
|||||||
/**
|
/**
|
||||||
* Build categorized hints list from all sources
|
* Build categorized hints list from all sources
|
||||||
* @param {Object} allVariables - All available variables
|
* @param {Object} allVariables - All available variables
|
||||||
|
* @param {string[]} anywordAutocompleteHints - Custom autocomplete hints
|
||||||
* @param {Object} options - Configuration options
|
* @param {Object} options - Configuration options
|
||||||
* @returns {Object} Categorized hints object
|
* @returns {Object} Categorized hints object
|
||||||
*/
|
*/
|
||||||
const buildCategorizedHintsList = (allVariables = {}, options = {}) => {
|
const buildCategorizedHintsList = (allVariables = {}, anywordAutocompleteHints = [], options = {}) => {
|
||||||
const categorizedHints = {
|
const categorizedHints = {
|
||||||
api: new Set(),
|
api: new Set(),
|
||||||
variables: new Set(),
|
variables: new Set(),
|
||||||
@@ -206,7 +207,7 @@ const buildCategorizedHintsList = (allVariables = {}, options = {}) => {
|
|||||||
// Add different types of hints
|
// Add different types of hints
|
||||||
addApiHintsToSet(categorizedHints.api, showHintsFor);
|
addApiHintsToSet(categorizedHints.api, showHintsFor);
|
||||||
addVariableHintsToSet(categorizedHints.variables, allVariables);
|
addVariableHintsToSet(categorizedHints.variables, allVariables);
|
||||||
addCustomHintsToSet(categorizedHints.anyword, options);
|
addCustomHintsToSet(categorizedHints.anyword, anywordAutocompleteHints);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
api: Array.from(categorizedHints.api).sort(),
|
api: Array.from(categorizedHints.api).sort(),
|
||||||
@@ -499,10 +500,11 @@ const createStandardHintList = (filteredHints, from, to) => {
|
|||||||
* Bruno AutoComplete Helper - Main function with context awareness
|
* Bruno AutoComplete Helper - Main function with context awareness
|
||||||
* @param {Object} cm - CodeMirror instance
|
* @param {Object} cm - CodeMirror instance
|
||||||
* @param {Object} allVariables - All available variables
|
* @param {Object} allVariables - All available variables
|
||||||
|
* @param {string[]} anywordAutocompleteHints - Custom autocomplete hints
|
||||||
* @param {Object} options - Configuration options
|
* @param {Object} options - Configuration options
|
||||||
* @returns {Object|null} Hint object or null
|
* @returns {Object|null} Hint object or null
|
||||||
*/
|
*/
|
||||||
export const getAutoCompleteHints = (cm, allVariables = {}, options = {}) => {
|
export const getAutoCompleteHints = (cm, allVariables = {}, anywordAutocompleteHints = [], options = {}) => {
|
||||||
if (!allVariables) {
|
if (!allVariables) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -520,7 +522,7 @@ export const getAutoCompleteHints = (cm, allVariables = {}, options = {}) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const categorizedHints = buildCategorizedHintsList(allVariables, options);
|
const categorizedHints = buildCategorizedHintsList(allVariables, anywordAutocompleteHints, options);
|
||||||
const filteredHints = filterHintsByContext(categorizedHints, word, context, showHintsFor);
|
const filteredHints = filterHintsByContext(categorizedHints, word, context, showHintsFor);
|
||||||
|
|
||||||
if (filteredHints.length === 0) {
|
if (filteredHints.length === 0) {
|
||||||
@@ -534,21 +536,75 @@ export const getAutoCompleteHints = (cm, allVariables = {}, options = {}) => {
|
|||||||
return createStandardHintList(filteredHints, from, to);
|
return createStandardHintList(filteredHints, from, to);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle click events for autocomplete
|
||||||
|
* @param {Object} cm - CodeMirror instance
|
||||||
|
* @param {Object} options - Configuration options
|
||||||
|
*/
|
||||||
|
const handleClickForAutocomplete = (cm, options) => {
|
||||||
|
const allVariables = options.getAllVariables?.() || {};
|
||||||
|
const anywordAutocompleteHints = options.getAnywordAutocompleteHints?.() || [];
|
||||||
|
const showHintsFor = options.showHintsFor || [];
|
||||||
|
|
||||||
|
// Build all available hints
|
||||||
|
const categorizedHints = buildCategorizedHintsList(allVariables, anywordAutocompleteHints, options);
|
||||||
|
|
||||||
|
// Combine all hints based on showHintsFor configuration
|
||||||
|
let allHints = [];
|
||||||
|
|
||||||
|
// Add API hints if enabled
|
||||||
|
const hasApiHints = showHintsFor.some(hint => ['req', 'res', 'bru'].includes(hint));
|
||||||
|
if (hasApiHints) {
|
||||||
|
allHints = [...allHints, ...categorizedHints.api];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add variable hints if enabled
|
||||||
|
if (showHintsFor.includes('variables')) {
|
||||||
|
allHints = [...allHints, ...categorizedHints.variables];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add anyword hints (always included)
|
||||||
|
allHints = [...allHints, ...categorizedHints.anyword];
|
||||||
|
|
||||||
|
// Remove duplicates and sort
|
||||||
|
allHints = [...new Set(allHints)].sort();
|
||||||
|
|
||||||
|
if (allHints.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cursor = cm.getCursor();
|
||||||
|
|
||||||
|
if (cursor.ch > 0) return;
|
||||||
|
|
||||||
|
// Defer showHint to ensure editor is focused
|
||||||
|
setTimeout(() => {
|
||||||
|
cm.showHint({
|
||||||
|
hint: () => ({
|
||||||
|
list: allHints,
|
||||||
|
from: cursor,
|
||||||
|
to: cursor
|
||||||
|
}),
|
||||||
|
completeSingle: false
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle keyup events for autocomplete
|
* Handle keyup events for autocomplete
|
||||||
* @param {Object} cm - CodeMirror instance
|
* @param {Object} cm - CodeMirror instance
|
||||||
* @param {Event} event - The keyup event
|
* @param {Event} event - The keyup event
|
||||||
* @param {Function} getAllVariablesFunc - Function to get all variables
|
|
||||||
* @param {Object} options - Configuration options
|
* @param {Object} options - Configuration options
|
||||||
*/
|
*/
|
||||||
const handleKeyupForAutocomplete = (cm, event, getAllVariablesFunc, options) => {
|
const handleKeyupForAutocomplete = (cm, event, options) => {
|
||||||
// Skip non-character keys
|
// Skip non-character keys
|
||||||
if (!NON_CHARACTER_KEYS.test(event?.key)) {
|
if (!NON_CHARACTER_KEYS.test(event?.key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allVariables = getAllVariablesFunc();
|
const allVariables = options.getAllVariables?.() || {};
|
||||||
const hints = getAutoCompleteHints(cm, allVariables, options);
|
const anywordAutocompleteHints = options.getAnywordAutocompleteHints?.() || [];
|
||||||
|
const hints = getAutoCompleteHints(cm, allVariables, anywordAutocompleteHints, options);
|
||||||
|
|
||||||
if (!hints) {
|
if (!hints) {
|
||||||
if (cm.state.completionActive) {
|
if (cm.state.completionActive) {
|
||||||
@@ -566,23 +622,37 @@ const handleKeyupForAutocomplete = (cm, event, getAllVariablesFunc, options) =>
|
|||||||
/**
|
/**
|
||||||
* Setup Bruno AutoComplete Helper on a CodeMirror editor
|
* Setup Bruno AutoComplete Helper on a CodeMirror editor
|
||||||
* @param {Object} editor - CodeMirror editor instance
|
* @param {Object} editor - CodeMirror editor instance
|
||||||
* @param {Function} getAllVariablesFunc - Function to get all variables
|
|
||||||
* @param {Object} options - Configuration options
|
* @param {Object} options - Configuration options
|
||||||
* @returns {Function} Cleanup function
|
* @returns {Function} Cleanup function
|
||||||
*/
|
*/
|
||||||
export const setupAutoComplete = (editor, getAllVariablesFunc, options = {}) => {
|
export const setupAutoComplete = (editor, options = {}) => {
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyupHandler = (cm, event) => {
|
const keyupHandler = (cm, event) => {
|
||||||
handleKeyupForAutocomplete(cm, event, getAllVariablesFunc, options);
|
handleKeyupForAutocomplete(cm, event, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
editor.on('keyup', keyupHandler);
|
editor.on('keyup', keyupHandler);
|
||||||
|
|
||||||
|
const clickHandler = (cm) => {
|
||||||
|
// Only show hints on click if the option is enabled and there's no active completion
|
||||||
|
if (options.showHintsOnClick) {
|
||||||
|
handleClickForAutocomplete(cm, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add click handler if showHintsOnClick is enabled
|
||||||
|
if (options.showHintsOnClick) {
|
||||||
|
editor.on('mousedown', clickHandler);
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
editor.off('keyup', keyupHandler);
|
editor.off('keyup', keyupHandler);
|
||||||
|
if (options.showHintsOnClick) {
|
||||||
|
editor.off('mousedown', clickHandler);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -43,7 +43,7 @@ describe('Bruno Autocomplete', () => {
|
|||||||
envVar2: 'value2',
|
envVar2: 'value2',
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
|
const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {
|
||||||
showHintsFor: ['variables']
|
showHintsFor: ['variables']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ describe('Bruno Autocomplete', () => {
|
|||||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 9 });
|
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 9 });
|
||||||
mockedCodemirror.getRange.mockReturnValue('{{$randomI');
|
mockedCodemirror.getRange.mockReturnValue('{{$randomI');
|
||||||
|
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, {}, {
|
const result = getAutoCompleteHints(mockedCodemirror, {}, [], {
|
||||||
showHintsFor: ['variables']
|
showHintsFor: ['variables']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ describe('Bruno Autocomplete', () => {
|
|||||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 14 });
|
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 14 });
|
||||||
mockedCodemirror.getRange.mockReturnValue('{{process.env.N');
|
mockedCodemirror.getRange.mockReturnValue('{{process.env.N');
|
||||||
|
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
|
const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {
|
||||||
showHintsFor: ['variables']
|
showHintsFor: ['variables']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ describe('Bruno Autocomplete', () => {
|
|||||||
path: 'value'
|
path: 'value'
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
|
const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {
|
||||||
showHintsFor: ['variables']
|
showHintsFor: ['variables']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ describe('Bruno Autocomplete', () => {
|
|||||||
'config.app.name': 'bruno'
|
'config.app.name': 'bruno'
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
|
const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {
|
||||||
showHintsFor: ['variables']
|
showHintsFor: ['variables']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ describe('Bruno Autocomplete', () => {
|
|||||||
mockedCodemirror.getLine.mockReturnValue(input);
|
mockedCodemirror.getLine.mockReturnValue(input);
|
||||||
mockedCodemirror.getRange.mockReturnValue(input);
|
mockedCodemirror.getRange.mockReturnValue(input);
|
||||||
|
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, {}, {
|
const result = getAutoCompleteHints(mockedCodemirror, {}, [], {
|
||||||
showHintsFor: ['req', 'res', 'bru']
|
showHintsFor: ['req', 'res', 'bru']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ describe('Bruno Autocomplete', () => {
|
|||||||
mockedCodemirror.getLine.mockReturnValue('req.get');
|
mockedCodemirror.getLine.mockReturnValue('req.get');
|
||||||
mockedCodemirror.getRange.mockReturnValue('req.get');
|
mockedCodemirror.getRange.mockReturnValue('req.get');
|
||||||
|
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, {}, {
|
const result = getAutoCompleteHints(mockedCodemirror, {}, [], {
|
||||||
showHintsFor: ['req']
|
showHintsFor: ['req']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ describe('Bruno Autocomplete', () => {
|
|||||||
mockedCodemirror.getLine.mockReturnValue('bru.runner.');
|
mockedCodemirror.getLine.mockReturnValue('bru.runner.');
|
||||||
mockedCodemirror.getRange.mockReturnValue('bru.runner.');
|
mockedCodemirror.getRange.mockReturnValue('bru.runner.');
|
||||||
|
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, {}, {
|
const result = getAutoCompleteHints(mockedCodemirror, {}, [], {
|
||||||
showHintsFor: ['bru']
|
showHintsFor: ['bru']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -234,11 +234,9 @@ describe('Bruno Autocomplete', () => {
|
|||||||
mockedCodemirror.getLine.mockReturnValue('Content-');
|
mockedCodemirror.getLine.mockReturnValue('Content-');
|
||||||
mockedCodemirror.getRange.mockReturnValue('Content-');
|
mockedCodemirror.getRange.mockReturnValue('Content-');
|
||||||
|
|
||||||
const options = {
|
const customHints = ['Content-Type', 'Content-Encoding', 'Content-Length'];
|
||||||
anywordAutocompleteHints: ['Content-Type', 'Content-Encoding', 'Content-Length']
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, {}, options, {
|
const result = getAutoCompleteHints(mockedCodemirror, {}, customHints, {
|
||||||
showHintsFor: ['variables']
|
showHintsFor: ['variables']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -253,11 +251,9 @@ describe('Bruno Autocomplete', () => {
|
|||||||
mockedCodemirror.getLine.mockReturnValue('utils.');
|
mockedCodemirror.getLine.mockReturnValue('utils.');
|
||||||
mockedCodemirror.getRange.mockReturnValue('utils.');
|
mockedCodemirror.getRange.mockReturnValue('utils.');
|
||||||
|
|
||||||
const options = {
|
const customHints = ['utils.string.trim', 'utils.string.capitalize', 'utils.array.map'];
|
||||||
anywordAutocompleteHints: ['utils.string.trim', 'utils.string.capitalize', 'utils.array.map']
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, {}, options, {
|
const result = getAutoCompleteHints(mockedCodemirror, {}, customHints, {
|
||||||
showHintsFor: ['variables']
|
showHintsFor: ['variables']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -277,18 +273,14 @@ describe('Bruno Autocomplete', () => {
|
|||||||
|
|
||||||
it('should respect showHintsFor option for excluding hints', () => {
|
it('should respect showHintsFor option for excluding hints', () => {
|
||||||
const options = { showHintsFor: ['res', 'bru'] };
|
const options = { showHintsFor: ['res', 'bru'] };
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, {}, options, {
|
const result = getAutoCompleteHints(mockedCodemirror, {}, [], options);
|
||||||
showHintsFor: ['req']
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show hints when included in showHintsFor', () => {
|
it('should show hints when included in showHintsFor', () => {
|
||||||
const options = { showHintsFor: ['req'] };
|
const options = { showHintsFor: ['req'] };
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, {}, options, {
|
const result = getAutoCompleteHints(mockedCodemirror, {}, [], options);
|
||||||
showHintsFor: ['req']
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
||||||
expect(result.list).toEqual(
|
expect(result.list).toEqual(
|
||||||
@@ -303,7 +295,7 @@ describe('Bruno Autocomplete', () => {
|
|||||||
const allVariables = { envVar1: 'value1' };
|
const allVariables = { envVar1: 'value1' };
|
||||||
const options = { showHintsFor: ['req', 'res', 'bru'] };
|
const options = { showHintsFor: ['req', 'res', 'bru'] };
|
||||||
|
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, allVariables, options);
|
const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], options);
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -318,7 +310,7 @@ describe('Bruno Autocomplete', () => {
|
|||||||
allVariables[`var${i}`] = `value${i}`;
|
allVariables[`var${i}`] = `value${i}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
|
const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {
|
||||||
showHintsFor: ['variables']
|
showHintsFor: ['variables']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -337,7 +329,7 @@ describe('Bruno Autocomplete', () => {
|
|||||||
'v.banana': 'value3'
|
'v.banana': 'value3'
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
|
const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {
|
||||||
showHintsFor: ['variables']
|
showHintsFor: ['variables']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -357,7 +349,7 @@ describe('Bruno Autocomplete', () => {
|
|||||||
mockedCodemirror.getLine.mockReturnValue(' ');
|
mockedCodemirror.getLine.mockReturnValue(' ');
|
||||||
mockedCodemirror.getRange.mockReturnValue('');
|
mockedCodemirror.getRange.mockReturnValue('');
|
||||||
|
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, {});
|
const result = getAutoCompleteHints(mockedCodemirror, {}, []);
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -367,8 +359,8 @@ describe('Bruno Autocomplete', () => {
|
|||||||
mockedCodemirror.getLine.mockReturnValue('{{varName}}');
|
mockedCodemirror.getLine.mockReturnValue('{{varName}}');
|
||||||
mockedCodemirror.getRange.mockReturnValue('{{varName');
|
mockedCodemirror.getRange.mockReturnValue('{{varName');
|
||||||
|
|
||||||
const emptyResult = getAutoCompleteHints(mockedCodemirror, {});
|
const emptyResult = getAutoCompleteHints(mockedCodemirror, {}, []);
|
||||||
const nullResult = getAutoCompleteHints(mockedCodemirror, null);
|
const nullResult = getAutoCompleteHints(mockedCodemirror, null, []);
|
||||||
|
|
||||||
expect(emptyResult).toBeNull();
|
expect(emptyResult).toBeNull();
|
||||||
expect(nullResult).toBeNull();
|
expect(nullResult).toBeNull();
|
||||||
@@ -380,7 +372,7 @@ describe('Bruno Autocomplete', () => {
|
|||||||
mockedCodemirror.getLine.mockReturnValue(line);
|
mockedCodemirror.getLine.mockReturnValue(line);
|
||||||
mockedCodemirror.getRange.mockReturnValue(line);
|
mockedCodemirror.getRange.mockReturnValue(line);
|
||||||
|
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, {}, {
|
const result = getAutoCompleteHints(mockedCodemirror, {}, [], {
|
||||||
showHintsFor: ['req']
|
showHintsFor: ['req']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -401,7 +393,7 @@ describe('Bruno Autocomplete', () => {
|
|||||||
VARIABLE3: 'value3'
|
VARIABLE3: 'value3'
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
|
const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {
|
||||||
showHintsFor: ['variables']
|
showHintsFor: ['variables']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -428,7 +420,8 @@ describe('Bruno Autocomplete', () => {
|
|||||||
|
|
||||||
describe('Setup and cleanup', () => {
|
describe('Setup and cleanup', () => {
|
||||||
it('should setup keyup event listener and return cleanup function', () => {
|
it('should setup keyup event listener and return cleanup function', () => {
|
||||||
cleanupFn = setupAutoComplete(mockedCodemirror, mockGetAllVariables);
|
const options = { getAllVariables: mockGetAllVariables };
|
||||||
|
cleanupFn = setupAutoComplete(mockedCodemirror, options);
|
||||||
|
|
||||||
expect(mockedCodemirror.on).toHaveBeenCalledWith('keyup', expect.any(Function));
|
expect(mockedCodemirror.on).toHaveBeenCalledWith('keyup', expect.any(Function));
|
||||||
expect(cleanupFn).toBeInstanceOf(Function);
|
expect(cleanupFn).toBeInstanceOf(Function);
|
||||||
@@ -438,7 +431,7 @@ describe('Bruno Autocomplete', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not setup if editor is null', () => {
|
it('should not setup if editor is null', () => {
|
||||||
const result = setupAutoComplete(null, mockGetAllVariables);
|
const result = setupAutoComplete(null, { getAllVariables: mockGetAllVariables });
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
expect(result).toBeUndefined();
|
||||||
expect(mockedCodemirror.on).not.toHaveBeenCalled();
|
expect(mockedCodemirror.on).not.toHaveBeenCalled();
|
||||||
@@ -447,9 +440,11 @@ describe('Bruno Autocomplete', () => {
|
|||||||
|
|
||||||
describe('Event handling', () => {
|
describe('Event handling', () => {
|
||||||
it('should trigger hints on character key press', () => {
|
it('should trigger hints on character key press', () => {
|
||||||
cleanupFn = setupAutoComplete(mockedCodemirror, mockGetAllVariables, {
|
const options = {
|
||||||
|
getAllVariables: mockGetAllVariables,
|
||||||
showHintsFor: ['req']
|
showHintsFor: ['req']
|
||||||
});
|
};
|
||||||
|
cleanupFn = setupAutoComplete(mockedCodemirror, options);
|
||||||
const keyupHandler = mockedCodemirror.on.mock.calls[0][1];
|
const keyupHandler = mockedCodemirror.on.mock.calls[0][1];
|
||||||
|
|
||||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 4 });
|
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 4 });
|
||||||
@@ -464,7 +459,8 @@ describe('Bruno Autocomplete', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not trigger hints on non-character keys', () => {
|
it('should not trigger hints on non-character keys', () => {
|
||||||
cleanupFn = setupAutoComplete(mockedCodemirror, mockGetAllVariables);
|
const options = { getAllVariables: mockGetAllVariables };
|
||||||
|
cleanupFn = setupAutoComplete(mockedCodemirror, options);
|
||||||
const keyupHandler = mockedCodemirror.on.mock.calls[0][1];
|
const keyupHandler = mockedCodemirror.on.mock.calls[0][1];
|
||||||
|
|
||||||
const nonCharacterKeys = ['Shift', 'Tab', 'Enter', 'Escape', 'ArrowUp', 'ArrowDown', 'Meta'];
|
const nonCharacterKeys = ['Shift', 'Tab', 'Enter', 'Escape', 'ArrowUp', 'ArrowDown', 'Meta'];
|
||||||
@@ -478,7 +474,8 @@ describe('Bruno Autocomplete', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should close existing completion when no hints available', () => {
|
it('should close existing completion when no hints available', () => {
|
||||||
cleanupFn = setupAutoComplete(mockedCodemirror, mockGetAllVariables);
|
const options = { getAllVariables: mockGetAllVariables };
|
||||||
|
cleanupFn = setupAutoComplete(mockedCodemirror, options);
|
||||||
const keyupHandler = mockedCodemirror.on.mock.calls[0][1];
|
const keyupHandler = mockedCodemirror.on.mock.calls[0][1];
|
||||||
|
|
||||||
const mockCompletion = { close: jest.fn() };
|
const mockCompletion = { close: jest.fn() };
|
||||||
@@ -495,8 +492,11 @@ describe('Bruno Autocomplete', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should pass options to getAutoCompleteHints', () => {
|
it('should pass options to getAutoCompleteHints', () => {
|
||||||
const options = { showHintsFor: ['req'] };
|
const options = {
|
||||||
cleanupFn = setupAutoComplete(mockedCodemirror, mockGetAllVariables, options);
|
getAllVariables: mockGetAllVariables,
|
||||||
|
showHintsFor: ['req']
|
||||||
|
};
|
||||||
|
cleanupFn = setupAutoComplete(mockedCodemirror, options);
|
||||||
const keyupHandler = mockedCodemirror.on.mock.calls[0][1];
|
const keyupHandler = mockedCodemirror.on.mock.calls[0][1];
|
||||||
|
|
||||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 4 });
|
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 4 });
|
||||||
@@ -512,6 +512,173 @@ describe('Bruno Autocomplete', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Click event handling (showHintsOnClick)', () => {
|
||||||
|
it('should setup mousedown event listener when showHintsOnClick is enabled', () => {
|
||||||
|
const options = {
|
||||||
|
getAllVariables: mockGetAllVariables,
|
||||||
|
showHintsOnClick: true
|
||||||
|
};
|
||||||
|
cleanupFn = setupAutoComplete(mockedCodemirror, options);
|
||||||
|
|
||||||
|
expect(mockedCodemirror.on).toHaveBeenCalledWith('keyup', expect.any(Function));
|
||||||
|
expect(mockedCodemirror.on).toHaveBeenCalledWith('mousedown', expect.any(Function));
|
||||||
|
expect(mockedCodemirror.on).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not setup mousedown event listener when showHintsOnClick is disabled', () => {
|
||||||
|
const options = {
|
||||||
|
getAllVariables: mockGetAllVariables,
|
||||||
|
showHintsOnClick: false
|
||||||
|
};
|
||||||
|
cleanupFn = setupAutoComplete(mockedCodemirror, options);
|
||||||
|
|
||||||
|
expect(mockedCodemirror.on).toHaveBeenCalledWith('keyup', expect.any(Function));
|
||||||
|
expect(mockedCodemirror.on).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not setup mousedown event listener when showHintsOnClick is undefined', () => {
|
||||||
|
const options = {
|
||||||
|
getAllVariables: mockGetAllVariables
|
||||||
|
};
|
||||||
|
cleanupFn = setupAutoComplete(mockedCodemirror, options);
|
||||||
|
|
||||||
|
expect(mockedCodemirror.on).toHaveBeenCalledWith('keyup', expect.any(Function));
|
||||||
|
expect(mockedCodemirror.on).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show hints on click when showHintsOnClick is enabled', () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
const mockGetAnywordAutocompleteHints = jest.fn(() => ['Content-Type', 'Accept']);
|
||||||
|
const options = {
|
||||||
|
getAllVariables: mockGetAllVariables,
|
||||||
|
getAnywordAutocompleteHints: mockGetAnywordAutocompleteHints,
|
||||||
|
showHintsOnClick: true,
|
||||||
|
showHintsFor: ['req', 'variables']
|
||||||
|
};
|
||||||
|
cleanupFn = setupAutoComplete(mockedCodemirror, options);
|
||||||
|
|
||||||
|
// Find the click handler (mousedown event)
|
||||||
|
const clickHandler = mockedCodemirror.on.mock.calls.find(call => call[0] === 'mousedown')[1];
|
||||||
|
|
||||||
|
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 0 });
|
||||||
|
|
||||||
|
clickHandler(mockedCodemirror);
|
||||||
|
|
||||||
|
// Run all timers to execute the setTimeout
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(mockGetAllVariables).toHaveBeenCalled();
|
||||||
|
expect(mockGetAnywordAutocompleteHints).toHaveBeenCalled();
|
||||||
|
expect(mockedCodemirror.showHint).toHaveBeenCalled();
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show hints on click when showHintsOnClick is disabled', () => {
|
||||||
|
const options = {
|
||||||
|
getAllVariables: mockGetAllVariables,
|
||||||
|
showHintsOnClick: false
|
||||||
|
};
|
||||||
|
cleanupFn = setupAutoComplete(mockedCodemirror, options);
|
||||||
|
|
||||||
|
// There should be no mousedown handler
|
||||||
|
const mousedownCalls = mockedCodemirror.on.mock.calls.filter(call => call[0] === 'mousedown');
|
||||||
|
expect(mousedownCalls).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cleanup mousedown event listener when showHintsOnClick was enabled', () => {
|
||||||
|
const options = {
|
||||||
|
getAllVariables: mockGetAllVariables,
|
||||||
|
showHintsOnClick: true
|
||||||
|
};
|
||||||
|
cleanupFn = setupAutoComplete(mockedCodemirror, options);
|
||||||
|
|
||||||
|
cleanupFn();
|
||||||
|
|
||||||
|
expect(mockedCodemirror.off).toHaveBeenCalledWith('keyup', expect.any(Function));
|
||||||
|
expect(mockedCodemirror.off).toHaveBeenCalledWith('mousedown', expect.any(Function));
|
||||||
|
expect(mockedCodemirror.off).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only cleanup keyup event listener when showHintsOnClick was disabled', () => {
|
||||||
|
const options = {
|
||||||
|
getAllVariables: mockGetAllVariables,
|
||||||
|
showHintsOnClick: false
|
||||||
|
};
|
||||||
|
cleanupFn = setupAutoComplete(mockedCodemirror, options);
|
||||||
|
|
||||||
|
cleanupFn();
|
||||||
|
|
||||||
|
expect(mockedCodemirror.off).toHaveBeenCalledWith('keyup', expect.any(Function));
|
||||||
|
expect(mockedCodemirror.off).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show all available hints on click based on showHintsFor configuration', () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
const mockGetAnywordAutocompleteHints = jest.fn(() => ['Content-Type', 'Accept']);
|
||||||
|
const options = {
|
||||||
|
getAllVariables: mockGetAllVariables.mockReturnValue({
|
||||||
|
envVar1: 'value1',
|
||||||
|
envVar2: 'value2'
|
||||||
|
}),
|
||||||
|
getAnywordAutocompleteHints: mockGetAnywordAutocompleteHints,
|
||||||
|
showHintsOnClick: true,
|
||||||
|
showHintsFor: ['req', 'variables']
|
||||||
|
};
|
||||||
|
cleanupFn = setupAutoComplete(mockedCodemirror, options);
|
||||||
|
|
||||||
|
// Find the click handler (mousedown event)
|
||||||
|
const clickHandler = mockedCodemirror.on.mock.calls.find(call => call[0] === 'mousedown')[1];
|
||||||
|
|
||||||
|
const mockCursor = { line: 0, ch: 0 };
|
||||||
|
mockedCodemirror.getCursor.mockReturnValue(mockCursor);
|
||||||
|
|
||||||
|
clickHandler(mockedCodemirror);
|
||||||
|
|
||||||
|
// Run all timers to execute the setTimeout
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(mockedCodemirror.showHint).toHaveBeenCalledWith({
|
||||||
|
hint: expect.any(Function),
|
||||||
|
completeSingle: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify the hint function returns the expected structure
|
||||||
|
const hintCall = mockedCodemirror.showHint.mock.calls[0][0];
|
||||||
|
const hintResult = hintCall.hint();
|
||||||
|
|
||||||
|
expect(hintResult).toEqual({
|
||||||
|
list: expect.any(Array),
|
||||||
|
from: mockCursor,
|
||||||
|
to: mockCursor
|
||||||
|
});
|
||||||
|
expect(hintResult.list.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show hints on click when no hints are available', () => {
|
||||||
|
const options = {
|
||||||
|
getAllVariables: mockGetAllVariables.mockReturnValue({}),
|
||||||
|
getAnywordAutocompleteHints: jest.fn(() => []),
|
||||||
|
showHintsOnClick: true,
|
||||||
|
showHintsFor: []
|
||||||
|
};
|
||||||
|
cleanupFn = setupAutoComplete(mockedCodemirror, options);
|
||||||
|
|
||||||
|
// Find the click handler (mousedown event)
|
||||||
|
const clickHandler = mockedCodemirror.on.mock.calls.find(call => call[0] === 'mousedown')[1];
|
||||||
|
|
||||||
|
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 0 });
|
||||||
|
|
||||||
|
clickHandler(mockedCodemirror);
|
||||||
|
|
||||||
|
expect(mockedCodemirror.showHint).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CodeMirror integration', () => {
|
describe('CodeMirror integration', () => {
|
||||||
|
@@ -233,7 +233,8 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
|||||||
name: si.name,
|
name: si.name,
|
||||||
filename: si.filename,
|
filename: si.filename,
|
||||||
seq: si.seq,
|
seq: si.seq,
|
||||||
settings: si.settings
|
settings: si.settings,
|
||||||
|
tags: si.tags
|
||||||
};
|
};
|
||||||
|
|
||||||
if (si.request) {
|
if (si.request) {
|
||||||
@@ -257,8 +258,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
|||||||
vars: si.request.vars,
|
vars: si.request.vars,
|
||||||
assertions: si.request.assertions,
|
assertions: si.request.assertions,
|
||||||
tests: si.request.tests,
|
tests: si.request.tests,
|
||||||
docs: si.request.docs,
|
docs: si.request.docs
|
||||||
tags: si.request.tags
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle auth object dynamically
|
// Handle auth object dynamically
|
||||||
@@ -555,6 +555,7 @@ export const transformRequestToSaveToFilesystem = (item) => {
|
|||||||
name: _item.name,
|
name: _item.name,
|
||||||
seq: _item.seq,
|
seq: _item.seq,
|
||||||
settings: _item.settings,
|
settings: _item.settings,
|
||||||
|
tags: _item.tags,
|
||||||
request: {
|
request: {
|
||||||
method: _item.request.method,
|
method: _item.request.method,
|
||||||
url: _item.request.url,
|
url: _item.request.url,
|
||||||
@@ -566,8 +567,7 @@ export const transformRequestToSaveToFilesystem = (item) => {
|
|||||||
vars: _item.request.vars,
|
vars: _item.request.vars,
|
||||||
assertions: _item.request.assertions,
|
assertions: _item.request.assertions,
|
||||||
tests: _item.request.tests,
|
tests: _item.request.tests,
|
||||||
docs: _item.request.docs,
|
docs: _item.request.docs
|
||||||
tags: _item.request.tags
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1108,3 +1108,20 @@ export const calculateDraggedItemNewPathname = ({ draggedItem, targetItem, dropT
|
|||||||
};
|
};
|
||||||
|
|
||||||
// item sequence utils - END
|
// item sequence utils - END
|
||||||
|
|
||||||
|
export const getUniqueTagsFromItems = (items = []) => {
|
||||||
|
const allTags = new Set();
|
||||||
|
const getTags = (items) => {
|
||||||
|
items.forEach(item => {
|
||||||
|
if (isItemARequest(item)) {
|
||||||
|
const tags = item.draft ? get(item, 'draft.tags', []) : get(item, 'tags', []);
|
||||||
|
tags.forEach(tag => allTags.add(tag));
|
||||||
|
}
|
||||||
|
if (item.items) {
|
||||||
|
getTags(item.items);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
getTags(items);
|
||||||
|
return Array.from(allTags).sort();
|
||||||
|
};
|
||||||
|
@@ -462,10 +462,6 @@ const handler = async function (argv) {
|
|||||||
console.error(chalk.red(`Path not found: ${resolvedPath}`));
|
console.error(chalk.red(`Path not found: ${resolvedPath}`));
|
||||||
process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND);
|
process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
requestItems = requestItems.filter((item) => {
|
|
||||||
return isRequestTagsIncluded(item.tags, includeTags, excludeTags);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
requestItems = getCallStack(resolvedPaths, collection, { recursive });
|
requestItems = getCallStack(resolvedPaths, collection, { recursive });
|
||||||
@@ -478,6 +474,10 @@ const handler = async function (argv) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestItems = requestItems.filter((item) => {
|
||||||
|
return isRequestTagsIncluded(item.tags, includeTags, excludeTags);
|
||||||
|
});
|
||||||
|
|
||||||
const runtime = getJsSandboxRuntime(sandbox);
|
const runtime = getJsSandboxRuntime(sandbox);
|
||||||
|
|
||||||
const runSingleRequestByPathname = async (relativeItemPathname) => {
|
const runSingleRequestByPathname = async (relativeItemPathname) => {
|
||||||
|
@@ -64,7 +64,7 @@ const bruToJson = (bru) => {
|
|||||||
name: _.get(json, 'meta.name'),
|
name: _.get(json, 'meta.name'),
|
||||||
seq: !_.isNaN(sequence) ? Number(sequence) : 1,
|
seq: !_.isNaN(sequence) ? Number(sequence) : 1,
|
||||||
settings: _.get(json, 'settings', {}),
|
settings: _.get(json, 'settings', {}),
|
||||||
tags: _.get(json, 'tags', []),
|
tags: _.get(json, 'meta.tags', []),
|
||||||
request: {
|
request: {
|
||||||
method: _.upperCase(_.get(json, 'http.method')),
|
method: _.upperCase(_.get(json, 'http.method')),
|
||||||
url: _.get(json, 'http.url'),
|
url: _.get(json, 'http.url'),
|
||||||
|
@@ -138,6 +138,7 @@ const bruToJson = (data, parsed = false) => {
|
|||||||
name: _.get(json, 'meta.name'),
|
name: _.get(json, 'meta.name'),
|
||||||
seq: !_.isNaN(sequence) ? Number(sequence) : 1,
|
seq: !_.isNaN(sequence) ? Number(sequence) : 1,
|
||||||
settings: _.get(json, 'settings', {}),
|
settings: _.get(json, 'settings', {}),
|
||||||
|
tags: _.get(json, 'meta.tags', []),
|
||||||
request: {
|
request: {
|
||||||
method: _.upperCase(_.get(json, 'http.method')),
|
method: _.upperCase(_.get(json, 'http.method')),
|
||||||
url: _.get(json, 'http.url'),
|
url: _.get(json, 'http.url'),
|
||||||
@@ -149,14 +150,12 @@ const bruToJson = (data, parsed = false) => {
|
|||||||
vars: _.get(json, 'vars', {}),
|
vars: _.get(json, 'vars', {}),
|
||||||
assertions: _.get(json, 'assertions', []),
|
assertions: _.get(json, 'assertions', []),
|
||||||
tests: _.get(json, 'tests', ''),
|
tests: _.get(json, 'tests', ''),
|
||||||
docs: _.get(json, 'docs', ''),
|
docs: _.get(json, 'docs', '')
|
||||||
tags: _.get(json, 'tags', [])
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
transformedJson.request.auth.mode = _.get(json, 'http.auth', 'none');
|
transformedJson.request.auth.mode = _.get(json, 'http.auth', 'none');
|
||||||
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
|
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
|
||||||
|
|
||||||
return transformedJson;
|
return transformedJson;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Promise.reject(e);
|
return Promise.reject(e);
|
||||||
@@ -196,7 +195,8 @@ const jsonToBru = async (json) => {
|
|||||||
meta: {
|
meta: {
|
||||||
name: _.get(json, 'name'),
|
name: _.get(json, 'name'),
|
||||||
type: type,
|
type: type,
|
||||||
seq: !_.isNaN(sequence) ? Number(sequence) : 1
|
seq: !_.isNaN(sequence) ? Number(sequence) : 1,
|
||||||
|
tags: _.get(json, 'tags', []),
|
||||||
},
|
},
|
||||||
http: {
|
http: {
|
||||||
method: _.lowerCase(_.get(json, 'request.method')),
|
method: _.lowerCase(_.get(json, 'request.method')),
|
||||||
@@ -216,8 +216,7 @@ const jsonToBru = async (json) => {
|
|||||||
assertions: _.get(json, 'request.assertions', []),
|
assertions: _.get(json, 'request.assertions', []),
|
||||||
tests: _.get(json, 'request.tests', ''),
|
tests: _.get(json, 'request.tests', ''),
|
||||||
settings: _.get(json, 'settings', {}),
|
settings: _.get(json, 'settings', {}),
|
||||||
docs: _.get(json, 'request.docs', ''),
|
docs: _.get(json, 'request.docs', '')
|
||||||
tags: _.get(json, 'request.tags', [])
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const bru = jsonToBruV2(bruJson);
|
const bru = jsonToBruV2(bruJson);
|
||||||
@@ -239,7 +238,8 @@ const jsonToBruViaWorker = async (json) => {
|
|||||||
meta: {
|
meta: {
|
||||||
name: _.get(json, 'name'),
|
name: _.get(json, 'name'),
|
||||||
type: type,
|
type: type,
|
||||||
seq: !_.isNaN(sequence) ? Number(sequence) : 1
|
seq: !_.isNaN(sequence) ? Number(sequence) : 1,
|
||||||
|
tags: _.get(json, 'tags', [])
|
||||||
},
|
},
|
||||||
http: {
|
http: {
|
||||||
method: _.lowerCase(_.get(json, 'request.method')),
|
method: _.lowerCase(_.get(json, 'request.method')),
|
||||||
@@ -259,8 +259,7 @@ const jsonToBruViaWorker = async (json) => {
|
|||||||
assertions: _.get(json, 'request.assertions', []),
|
assertions: _.get(json, 'request.assertions', []),
|
||||||
tests: _.get(json, 'request.tests', ''),
|
tests: _.get(json, 'request.tests', ''),
|
||||||
settings: _.get(json, 'settings', {}),
|
settings: _.get(json, 'settings', {}),
|
||||||
docs: _.get(json, 'request.docs', ''),
|
docs: _.get(json, 'request.docs', '')
|
||||||
tags: _.get(json, 'request.tags', [])
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const bru = await bruParserWorker?.jsonToBru(bruJson)
|
const bru = await bruParserWorker?.jsonToBru(bruJson)
|
||||||
|
@@ -1017,8 +1017,8 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
if (tags && tags.include && tags.exclude) {
|
if (tags && tags.include && tags.exclude) {
|
||||||
const includeTags = tags.include ? tags.include : [];
|
const includeTags = tags.include ? tags.include : [];
|
||||||
const excludeTags = tags.exclude ? tags.exclude : [];
|
const excludeTags = tags.exclude ? tags.exclude : [];
|
||||||
folderRequests = folderRequests.filter(({ request }) => {
|
folderRequests = folderRequests.filter(({ tags }) => {
|
||||||
return isRequestTagsIncluded(request.tags, includeTags, excludeTags)
|
return isRequestTagsIncluded(tags, includeTags, excludeTags)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -52,7 +52,7 @@ const grammar = ohm.grammar(`Bru {
|
|||||||
pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*
|
pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*
|
||||||
pair = st* key st* ":" st* value st*
|
pair = st* key st* ":" st* value st*
|
||||||
key = keychar*
|
key = keychar*
|
||||||
value = multilinetextblock | valuechar*
|
value = list | multilinetextblock | valuechar*
|
||||||
|
|
||||||
// Dictionary for Assert Block
|
// Dictionary for Assert Block
|
||||||
assertdictionary = st* "{" assertpairlist? tagend
|
assertdictionary = st* "{" assertpairlist? tagend
|
||||||
@@ -67,12 +67,11 @@ const grammar = ohm.grammar(`Bru {
|
|||||||
textchar = ~nl any
|
textchar = ~nl any
|
||||||
|
|
||||||
// List
|
// List
|
||||||
listend = nl "]"
|
listend = stnl* "]"
|
||||||
list = st* "[" listitems? listend
|
list = st* "[" listitems? listend
|
||||||
listitems = (~listend nl)* listitem (~listend stnl* listitem)* (~listend space)*
|
listitems = (~listend stnl)* listitem (~listend stnl* listitem)* (~listend space)*
|
||||||
listitem = st* textchar+ st*
|
listitem = st* textchar+ st*
|
||||||
|
|
||||||
tags = "tags" list
|
|
||||||
meta = "meta" dictionary
|
meta = "meta" dictionary
|
||||||
settings = "settings" dictionary
|
settings = "settings" dictionary
|
||||||
|
|
||||||
@@ -276,6 +275,10 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
|||||||
},
|
},
|
||||||
pair(_1, key, _2, _3, _4, value, _5) {
|
pair(_1, key, _2, _3, _4, value, _5) {
|
||||||
let res = {};
|
let res = {};
|
||||||
|
if (Array.isArray(value.ast)) {
|
||||||
|
res[key.ast] = value.ast;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
res[key.ast] = value.ast ? value.ast.trim() : '';
|
res[key.ast] = value.ast ? value.ast.trim() : '';
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
@@ -283,6 +286,9 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
|||||||
return chars.sourceString ? chars.sourceString.trim() : '';
|
return chars.sourceString ? chars.sourceString.trim() : '';
|
||||||
},
|
},
|
||||||
value(chars) {
|
value(chars) {
|
||||||
|
if (chars.ctorName === 'list') {
|
||||||
|
return chars.ast;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
let isMultiline = chars.sourceString?.startsWith(`'''`) && chars.sourceString?.endsWith(`'''`);
|
let isMultiline = chars.sourceString?.startsWith(`'''`) && chars.sourceString?.endsWith(`'''`);
|
||||||
if (isMultiline) {
|
if (isMultiline) {
|
||||||
@@ -342,9 +348,6 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
|||||||
_iter(...elements) {
|
_iter(...elements) {
|
||||||
return elements.map((e) => e.ast);
|
return elements.map((e) => e.ast);
|
||||||
},
|
},
|
||||||
tags(_1, list) {
|
|
||||||
return { tags: list.ast };
|
|
||||||
},
|
|
||||||
meta(_1, dictionary) {
|
meta(_1, dictionary) {
|
||||||
let meta = mapPairListToKeyValPair(dictionary.ast);
|
let meta = mapPairListToKeyValPair(dictionary.ast);
|
||||||
|
|
||||||
|
@@ -36,18 +36,23 @@ const jsonToBru = (json) => {
|
|||||||
|
|
||||||
if (meta) {
|
if (meta) {
|
||||||
bru += 'meta {\n';
|
bru += 'meta {\n';
|
||||||
|
|
||||||
|
const tags = meta.tags;
|
||||||
|
delete meta.tags;
|
||||||
|
|
||||||
for (const key in meta) {
|
for (const key in meta) {
|
||||||
bru += ` ${key}: ${meta[key]}\n`;
|
bru += ` ${key}: ${meta[key]}\n`;
|
||||||
}
|
}
|
||||||
bru += '}\n\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tags) {
|
if (tags && tags.length) {
|
||||||
bru += 'tags [\n';
|
bru += ` tags: [\n`;
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
bru += ` ${tag}\n`;
|
bru += ` ${tag}\n`;
|
||||||
}
|
}
|
||||||
bru += ']\n\n';
|
bru += ` ]\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
bru += '}\n\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (http && http.method) {
|
if (http && http.method) {
|
||||||
|
@@ -2,12 +2,11 @@ meta {
|
|||||||
name: Send Bulk SMS
|
name: Send Bulk SMS
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 1
|
||||||
}
|
tags: [
|
||||||
|
|
||||||
tags [
|
|
||||||
foo
|
foo
|
||||||
bar
|
bar
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
url: https://api.textlocal.in/send/:id
|
url: https://api.textlocal.in/send/:id
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
"meta": {
|
"meta": {
|
||||||
"name": "Send Bulk SMS",
|
"name": "Send Bulk SMS",
|
||||||
"type": "http",
|
"type": "http",
|
||||||
"seq": "1"
|
"seq": "1",
|
||||||
|
"tags": ["foo", "bar"]
|
||||||
},
|
},
|
||||||
"tags": ["foo", "bar"],
|
|
||||||
"http": {
|
"http": {
|
||||||
"method": "get",
|
"method": "get",
|
||||||
"url": "https://api.textlocal.in/send/:id",
|
"url": "https://api.textlocal.in/send/:id",
|
||||||
|
33
packages/bruno-lang/v2/tests/tags.spec.js
Normal file
33
packages/bruno-lang/v2/tests/tags.spec.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* This test file is used to test the text parser.
|
||||||
|
*/
|
||||||
|
const parser = require('../src/bruToJson');
|
||||||
|
|
||||||
|
describe('tags parser', () => {
|
||||||
|
it('should parse request tags', () => {
|
||||||
|
const input = `
|
||||||
|
meta {
|
||||||
|
name: request
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
tags: [
|
||||||
|
tag_1
|
||||||
|
tag_2
|
||||||
|
tag_3
|
||||||
|
tag_4
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const output = parser(input);
|
||||||
|
const expected = {
|
||||||
|
meta: {
|
||||||
|
name: 'request',
|
||||||
|
type: 'http',
|
||||||
|
tags: ['tag_1', 'tag_2', 'tag_3', 'tag_4'],
|
||||||
|
seq: '1'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(output).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
@@ -310,8 +310,7 @@ const requestSchema = Yup.object({
|
|||||||
.nullable(),
|
.nullable(),
|
||||||
assertions: Yup.array().of(keyValueSchema).nullable(),
|
assertions: Yup.array().of(keyValueSchema).nullable(),
|
||||||
tests: Yup.string().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)
|
.noUnknown(true)
|
||||||
.strict();
|
.strict();
|
||||||
@@ -356,6 +355,7 @@ const itemSchema = Yup.object({
|
|||||||
type: Yup.string().oneOf(['http-request', 'graphql-request', 'folder', 'js']).required('type is required'),
|
type: Yup.string().oneOf(['http-request', 'graphql-request', 'folder', 'js']).required('type is required'),
|
||||||
seq: Yup.number().min(1),
|
seq: Yup.number().min(1),
|
||||||
name: Yup.string().min(1, 'name must be at least 1 character').required('name is required'),
|
name: Yup.string().min(1, 'name must be at least 1 character').required('name is required'),
|
||||||
|
tags: Yup.array().of(Yup.string().matches(/^[\w-]+$/, 'tag must be alphanumeric')),
|
||||||
request: requestSchema.when('type', {
|
request: requestSchema.when('type', {
|
||||||
is: (type) => ['http-request', 'graphql-request'].includes(type),
|
is: (type) => ['http-request', 'graphql-request'].includes(type),
|
||||||
then: (schema) => schema.required('request is required when item-type is request')
|
then: (schema) => schema.required('request is required when item-type is request')
|
||||||
|
@@ -7,7 +7,8 @@ describe('Item Schema Validation', () => {
|
|||||||
const item = {
|
const item = {
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
name: 'A Folder',
|
name: 'A Folder',
|
||||||
type: 'folder'
|
type: 'folder',
|
||||||
|
tags: ['smoke-test']
|
||||||
};
|
};
|
||||||
|
|
||||||
const isValid = await itemSchema.validate(item);
|
const isValid = await itemSchema.validate(item);
|
||||||
|
@@ -9,7 +9,6 @@ describe('Request Schema Validation', () => {
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: [],
|
headers: [],
|
||||||
params: [],
|
params: [],
|
||||||
tags: ['smoke-test'],
|
|
||||||
body: {
|
body: {
|
||||||
mode: 'none'
|
mode: 'none'
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user