+
{this.secretEye(this.props.isSecret)}
diff --git a/packages/bruno-app/src/components/TagList/StyledWrapper.js b/packages/bruno-app/src/components/TagList/StyledWrapper.js
new file mode 100644
index 000000000..c81897a27
--- /dev/null
+++ b/packages/bruno-app/src/components/TagList/StyledWrapper.js
@@ -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;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/TagList/index.js b/packages/bruno-app/src/components/TagList/index.js
new file mode 100644
index 000000000..3e13637f3
--- /dev/null
+++ b/packages/bruno-app/src/components/TagList/index.js
@@ -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 (
+
+
+ {error && {error}}
+
+ {tags && tags.length
+ ? tags.map((_tag) => (
+ -
+
+
+ ))
+ : null}
+
+
+ );
+};
+
+export default TagList;
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
index 1a6de82f1..6056c20f7 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -20,6 +20,7 @@ import { getSubdirectoriesFromRoot } from 'utils/common/platform';
import toast from 'react-hot-toast';
import mime from 'mime-types';
import path from 'utils/common/path';
+import { getUniqueTagsFromItems } from 'utils/collections/index';
const initialState = {
collections: [],
@@ -36,6 +37,7 @@ export const collectionsSlice = createSlice({
collection.settingsSelectedTab = 'overview';
collection.folderLevelSettingsSelectedTab = {};
+ collection.allTags = []; // Initialize collection-level tags
// Collection mount status is used to track the mount status of the collection
// values can be 'unmounted', 'mounting', 'mounted'
@@ -1860,6 +1862,7 @@ export const collectionsSlice = createSlice({
currentItem.name = file.data.name;
currentItem.type = file.data.type;
currentItem.seq = file.data.seq;
+ currentItem.tags = file.data.tags;
currentItem.request = file.data.request;
currentItem.filename = file.meta.name;
currentItem.pathname = file.meta.pathname;
@@ -1875,6 +1878,7 @@ export const collectionsSlice = createSlice({
name: file.data.name,
type: file.data.type,
seq: file.data.seq,
+ tags: file.data.tags,
request: file.data.request,
settings: file.data.settings,
filename: file.meta.name,
@@ -1965,6 +1969,7 @@ export const collectionsSlice = createSlice({
item.name = file.data.name;
item.type = file.data.type;
item.seq = file.data.seq;
+ item.tags = file.data.tags;
item.request = file.data.request;
item.settings = file.data.settings;
item.filename = file.meta.name;
@@ -2224,6 +2229,20 @@ export const collectionsSlice = createSlice({
if (collection) {
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) => {
@@ -2352,10 +2371,12 @@ export const collectionsSlice = createSlice({
if (!item.draft) {
item.draft = cloneDeep(item);
}
- item.draft.request.tags = item.draft.request.tags || [];
- if (!item.draft.request.tags.includes(tag.trim())) {
- item.draft.request.tags.push(tag.trim());
+ item.draft.tags = item.draft.tags || [];
+ if (!item.draft.tags.includes(tag.trim())) {
+ item.draft.tags.push(tag.trim());
}
+
+ collection.allTags = getUniqueTagsFromItems(collection.items);
}
}
},
@@ -2370,10 +2391,20 @@ export const collectionsSlice = createSlice({
if (!item.draft) {
item.draft = cloneDeep(item);
}
- item.draft.request.tags = item.draft.request.tags || [];
- item.draft.request.tags = item.draft.request.tags.filter((t) => t !== tag.trim());
+ item.draft.tags = item.draft.tags || [];
+ 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,
runFolderEvent,
resetCollectionRunner,
+ updateRunnerTagsDetails,
updateRequestDocs,
updateFolderDocs,
moveCollection,
@@ -2492,7 +2524,8 @@ export const {
updateFolderAuth,
updateFolderAuthMode,
addRequestTag,
- deleteRequestTag
+ deleteRequestTag,
+ updateCollectionTagsList
} = collectionsSlice.actions;
export default collectionsSlice.reducer;
diff --git a/packages/bruno-app/src/utils/codemirror/autocomplete.js b/packages/bruno-app/src/utils/codemirror/autocomplete.js
index 93ad76493..7b65f4234 100644
--- a/packages/bruno-app/src/utils/codemirror/autocomplete.js
+++ b/packages/bruno-app/src/utils/codemirror/autocomplete.js
@@ -178,11 +178,11 @@ const addVariableHintsToSet = (variableHints, allVariables) => {
/**
* Add custom hints to categorized hints
* @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) => {
- if (options.anywordAutocompleteHints && Array.isArray(options.anywordAutocompleteHints)) {
- options.anywordAutocompleteHints.forEach(hint => {
+const addCustomHintsToSet = (anywordHints, customHints) => {
+ if (customHints && Array.isArray(customHints)) {
+ customHints.forEach(hint => {
generateProgressiveHints(hint).forEach(h => anywordHints.add(h));
});
}
@@ -191,10 +191,11 @@ const addCustomHintsToSet = (anywordHints, options) => {
/**
* Build categorized hints list from all sources
* @param {Object} allVariables - All available variables
+ * @param {string[]} anywordAutocompleteHints - Custom autocomplete hints
* @param {Object} options - Configuration options
* @returns {Object} Categorized hints object
*/
-const buildCategorizedHintsList = (allVariables = {}, options = {}) => {
+const buildCategorizedHintsList = (allVariables = {}, anywordAutocompleteHints = [], options = {}) => {
const categorizedHints = {
api: new Set(),
variables: new Set(),
@@ -206,7 +207,7 @@ const buildCategorizedHintsList = (allVariables = {}, options = {}) => {
// Add different types of hints
addApiHintsToSet(categorizedHints.api, showHintsFor);
addVariableHintsToSet(categorizedHints.variables, allVariables);
- addCustomHintsToSet(categorizedHints.anyword, options);
+ addCustomHintsToSet(categorizedHints.anyword, anywordAutocompleteHints);
return {
api: Array.from(categorizedHints.api).sort(),
@@ -499,10 +500,11 @@ const createStandardHintList = (filteredHints, from, to) => {
* Bruno AutoComplete Helper - Main function with context awareness
* @param {Object} cm - CodeMirror instance
* @param {Object} allVariables - All available variables
+ * @param {string[]} anywordAutocompleteHints - Custom autocomplete hints
* @param {Object} options - Configuration options
* @returns {Object|null} Hint object or null
*/
-export const getAutoCompleteHints = (cm, allVariables = {}, options = {}) => {
+export const getAutoCompleteHints = (cm, allVariables = {}, anywordAutocompleteHints = [], options = {}) => {
if (!allVariables) {
return null;
}
@@ -513,14 +515,14 @@ export const getAutoCompleteHints = (cm, allVariables = {}, options = {}) => {
}
const { word, from, to, context, requiresBraces } = wordInfo;
- const showHintsFor = options.showHintsFor || [];
+ const showHintsFor = options.showHintsFor || [];
// Check if this context requires braces but we're not in a brace context
if (context === 'variables' && !requiresBraces) {
return null;
}
- const categorizedHints = buildCategorizedHintsList(allVariables, options);
+ const categorizedHints = buildCategorizedHintsList(allVariables, anywordAutocompleteHints, options);
const filteredHints = filterHintsByContext(categorizedHints, word, context, showHintsFor);
if (filteredHints.length === 0) {
@@ -534,21 +536,75 @@ export const getAutoCompleteHints = (cm, allVariables = {}, options = {}) => {
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
* @param {Object} cm - CodeMirror instance
* @param {Event} event - The keyup event
- * @param {Function} getAllVariablesFunc - Function to get all variables
* @param {Object} options - Configuration options
*/
-const handleKeyupForAutocomplete = (cm, event, getAllVariablesFunc, options) => {
+const handleKeyupForAutocomplete = (cm, event, options) => {
// Skip non-character keys
if (!NON_CHARACTER_KEYS.test(event?.key)) {
return;
}
- const allVariables = getAllVariablesFunc();
- const hints = getAutoCompleteHints(cm, allVariables, options);
+ const allVariables = options.getAllVariables?.() || {};
+ const anywordAutocompleteHints = options.getAnywordAutocompleteHints?.() || [];
+ const hints = getAutoCompleteHints(cm, allVariables, anywordAutocompleteHints, options);
if (!hints) {
if (cm.state.completionActive) {
@@ -566,23 +622,37 @@ const handleKeyupForAutocomplete = (cm, event, getAllVariablesFunc, options) =>
/**
* Setup Bruno AutoComplete Helper on a CodeMirror editor
* @param {Object} editor - CodeMirror editor instance
- * @param {Function} getAllVariablesFunc - Function to get all variables
* @param {Object} options - Configuration options
* @returns {Function} Cleanup function
*/
-export const setupAutoComplete = (editor, getAllVariablesFunc, options = {}) => {
+export const setupAutoComplete = (editor, options = {}) => {
if (!editor) {
return;
}
const keyupHandler = (cm, event) => {
- handleKeyupForAutocomplete(cm, event, getAllVariablesFunc, options);
+ handleKeyupForAutocomplete(cm, event, options);
};
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 () => {
editor.off('keyup', keyupHandler);
+ if (options.showHintsOnClick) {
+ editor.off('mousedown', clickHandler);
+ }
};
};
diff --git a/packages/bruno-app/src/utils/codemirror/autocomplete.spec.js b/packages/bruno-app/src/utils/codemirror/autocomplete.spec.js
index a14f05917..5a8d984c1 100644
--- a/packages/bruno-app/src/utils/codemirror/autocomplete.spec.js
+++ b/packages/bruno-app/src/utils/codemirror/autocomplete.spec.js
@@ -43,7 +43,7 @@ describe('Bruno Autocomplete', () => {
envVar2: 'value2',
};
- const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
+ const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {
showHintsFor: ['variables']
});
@@ -60,7 +60,7 @@ describe('Bruno Autocomplete', () => {
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 9 });
mockedCodemirror.getRange.mockReturnValue('{{$randomI');
- const result = getAutoCompleteHints(mockedCodemirror, {}, {
+ const result = getAutoCompleteHints(mockedCodemirror, {}, [], {
showHintsFor: ['variables']
});
@@ -84,7 +84,7 @@ describe('Bruno Autocomplete', () => {
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 14 });
mockedCodemirror.getRange.mockReturnValue('{{process.env.N');
- const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
+ const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {
showHintsFor: ['variables']
});
@@ -106,7 +106,7 @@ describe('Bruno Autocomplete', () => {
path: 'value'
};
- const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
+ const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {
showHintsFor: ['variables']
});
@@ -134,7 +134,7 @@ describe('Bruno Autocomplete', () => {
'config.app.name': 'bruno'
};
- const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
+ const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {
showHintsFor: ['variables']
});
@@ -174,7 +174,7 @@ describe('Bruno Autocomplete', () => {
mockedCodemirror.getLine.mockReturnValue(input);
mockedCodemirror.getRange.mockReturnValue(input);
- const result = getAutoCompleteHints(mockedCodemirror, {}, {
+ const result = getAutoCompleteHints(mockedCodemirror, {}, [], {
showHintsFor: ['req', 'res', 'bru']
});
@@ -188,7 +188,7 @@ describe('Bruno Autocomplete', () => {
mockedCodemirror.getLine.mockReturnValue('req.get');
mockedCodemirror.getRange.mockReturnValue('req.get');
- const result = getAutoCompleteHints(mockedCodemirror, {}, {
+ const result = getAutoCompleteHints(mockedCodemirror, {}, [], {
showHintsFor: ['req']
});
@@ -213,7 +213,7 @@ describe('Bruno Autocomplete', () => {
mockedCodemirror.getLine.mockReturnValue('bru.runner.');
mockedCodemirror.getRange.mockReturnValue('bru.runner.');
- const result = getAutoCompleteHints(mockedCodemirror, {}, {
+ const result = getAutoCompleteHints(mockedCodemirror, {}, [], {
showHintsFor: ['bru']
});
@@ -234,11 +234,9 @@ describe('Bruno Autocomplete', () => {
mockedCodemirror.getLine.mockReturnValue('Content-');
mockedCodemirror.getRange.mockReturnValue('Content-');
- const options = {
- anywordAutocompleteHints: ['Content-Type', 'Content-Encoding', 'Content-Length']
- };
+ const customHints = ['Content-Type', 'Content-Encoding', 'Content-Length'];
- const result = getAutoCompleteHints(mockedCodemirror, {}, options, {
+ const result = getAutoCompleteHints(mockedCodemirror, {}, customHints, {
showHintsFor: ['variables']
});
@@ -253,11 +251,9 @@ describe('Bruno Autocomplete', () => {
mockedCodemirror.getLine.mockReturnValue('utils.');
mockedCodemirror.getRange.mockReturnValue('utils.');
- const options = {
- anywordAutocompleteHints: ['utils.string.trim', 'utils.string.capitalize', 'utils.array.map']
- };
+ const customHints = ['utils.string.trim', 'utils.string.capitalize', 'utils.array.map'];
- const result = getAutoCompleteHints(mockedCodemirror, {}, options, {
+ const result = getAutoCompleteHints(mockedCodemirror, {}, customHints, {
showHintsFor: ['variables']
});
@@ -277,18 +273,14 @@ describe('Bruno Autocomplete', () => {
it('should respect showHintsFor option for excluding hints', () => {
const options = { showHintsFor: ['res', 'bru'] };
- const result = getAutoCompleteHints(mockedCodemirror, {}, options, {
- showHintsFor: ['req']
- });
+ const result = getAutoCompleteHints(mockedCodemirror, {}, [], options);
expect(result).toBeNull();
});
it('should show hints when included in showHintsFor', () => {
const options = { showHintsFor: ['req'] };
- const result = getAutoCompleteHints(mockedCodemirror, {}, options, {
- showHintsFor: ['req']
- });
+ const result = getAutoCompleteHints(mockedCodemirror, {}, [], options);
expect(result).toBeTruthy();
expect(result.list).toEqual(
@@ -303,7 +295,7 @@ describe('Bruno Autocomplete', () => {
const allVariables = { envVar1: 'value1' };
const options = { showHintsFor: ['req', 'res', 'bru'] };
- const result = getAutoCompleteHints(mockedCodemirror, allVariables, options);
+ const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], options);
expect(result).toBeNull();
});
@@ -318,7 +310,7 @@ describe('Bruno Autocomplete', () => {
allVariables[`var${i}`] = `value${i}`;
}
- const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
+ const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {
showHintsFor: ['variables']
});
@@ -337,7 +329,7 @@ describe('Bruno Autocomplete', () => {
'v.banana': 'value3'
};
- const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
+ const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {
showHintsFor: ['variables']
});
@@ -357,7 +349,7 @@ describe('Bruno Autocomplete', () => {
mockedCodemirror.getLine.mockReturnValue(' ');
mockedCodemirror.getRange.mockReturnValue('');
- const result = getAutoCompleteHints(mockedCodemirror, {});
+ const result = getAutoCompleteHints(mockedCodemirror, {}, []);
expect(result).toBeNull();
});
@@ -367,8 +359,8 @@ describe('Bruno Autocomplete', () => {
mockedCodemirror.getLine.mockReturnValue('{{varName}}');
mockedCodemirror.getRange.mockReturnValue('{{varName');
- const emptyResult = getAutoCompleteHints(mockedCodemirror, {});
- const nullResult = getAutoCompleteHints(mockedCodemirror, null);
+ const emptyResult = getAutoCompleteHints(mockedCodemirror, {}, []);
+ const nullResult = getAutoCompleteHints(mockedCodemirror, null, []);
expect(emptyResult).toBeNull();
expect(nullResult).toBeNull();
@@ -380,7 +372,7 @@ describe('Bruno Autocomplete', () => {
mockedCodemirror.getLine.mockReturnValue(line);
mockedCodemirror.getRange.mockReturnValue(line);
- const result = getAutoCompleteHints(mockedCodemirror, {}, {
+ const result = getAutoCompleteHints(mockedCodemirror, {}, [], {
showHintsFor: ['req']
});
@@ -401,7 +393,7 @@ describe('Bruno Autocomplete', () => {
VARIABLE3: 'value3'
};
- const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
+ const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {
showHintsFor: ['variables']
});
@@ -428,7 +420,8 @@ describe('Bruno Autocomplete', () => {
describe('Setup and cleanup', () => {
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(cleanupFn).toBeInstanceOf(Function);
@@ -438,7 +431,7 @@ describe('Bruno Autocomplete', () => {
});
it('should not setup if editor is null', () => {
- const result = setupAutoComplete(null, mockGetAllVariables);
+ const result = setupAutoComplete(null, { getAllVariables: mockGetAllVariables });
expect(result).toBeUndefined();
expect(mockedCodemirror.on).not.toHaveBeenCalled();
@@ -447,9 +440,11 @@ describe('Bruno Autocomplete', () => {
describe('Event handling', () => {
it('should trigger hints on character key press', () => {
- cleanupFn = setupAutoComplete(mockedCodemirror, mockGetAllVariables, {
- showHintsFor: ['req']
- });
+ const options = {
+ getAllVariables: mockGetAllVariables,
+ showHintsFor: ['req']
+ };
+ cleanupFn = setupAutoComplete(mockedCodemirror, options);
const keyupHandler = mockedCodemirror.on.mock.calls[0][1];
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 4 });
@@ -464,7 +459,8 @@ describe('Bruno Autocomplete', () => {
});
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 nonCharacterKeys = ['Shift', 'Tab', 'Enter', 'Escape', 'ArrowUp', 'ArrowDown', 'Meta'];
@@ -478,7 +474,8 @@ describe('Bruno Autocomplete', () => {
});
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 mockCompletion = { close: jest.fn() };
@@ -495,8 +492,11 @@ describe('Bruno Autocomplete', () => {
});
it('should pass options to getAutoCompleteHints', () => {
- const options = { showHintsFor: ['req'] };
- cleanupFn = setupAutoComplete(mockedCodemirror, mockGetAllVariables, options);
+ const options = {
+ getAllVariables: mockGetAllVariables,
+ showHintsFor: ['req']
+ };
+ cleanupFn = setupAutoComplete(mockedCodemirror, options);
const keyupHandler = mockedCodemirror.on.mock.calls[0][1];
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', () => {
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index 45e03beff..69918c218 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -233,7 +233,8 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
name: si.name,
filename: si.filename,
seq: si.seq,
- settings: si.settings
+ settings: si.settings,
+ tags: si.tags
};
if (si.request) {
@@ -257,8 +258,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
vars: si.request.vars,
assertions: si.request.assertions,
tests: si.request.tests,
- docs: si.request.docs,
- tags: si.request.tags
+ docs: si.request.docs
};
// Handle auth object dynamically
@@ -555,6 +555,7 @@ export const transformRequestToSaveToFilesystem = (item) => {
name: _item.name,
seq: _item.seq,
settings: _item.settings,
+ tags: _item.tags,
request: {
method: _item.request.method,
url: _item.request.url,
@@ -566,8 +567,7 @@ export const transformRequestToSaveToFilesystem = (item) => {
vars: _item.request.vars,
assertions: _item.request.assertions,
tests: _item.request.tests,
- docs: _item.request.docs,
- tags: _item.request.tags
+ docs: _item.request.docs
}
};
@@ -1108,3 +1108,20 @@ export const calculateDraggedItemNewPathname = ({ draggedItem, targetItem, dropT
};
// 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();
+};
diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js
index 1adde3e1e..811314d41 100644
--- a/packages/bruno-cli/src/commands/run.js
+++ b/packages/bruno-cli/src/commands/run.js
@@ -462,10 +462,6 @@ const handler = async function (argv) {
console.error(chalk.red(`Path not found: ${resolvedPath}`));
process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND);
}
-
- requestItems = requestItems.filter((item) => {
- return isRequestTagsIncluded(item.tags, includeTags, excludeTags);
- });
}
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 runSingleRequestByPathname = async (relativeItemPathname) => {
diff --git a/packages/bruno-cli/src/utils/bru.js b/packages/bruno-cli/src/utils/bru.js
index ef3ca9137..b709f76f9 100644
--- a/packages/bruno-cli/src/utils/bru.js
+++ b/packages/bruno-cli/src/utils/bru.js
@@ -64,7 +64,7 @@ const bruToJson = (bru) => {
name: _.get(json, 'meta.name'),
seq: !_.isNaN(sequence) ? Number(sequence) : 1,
settings: _.get(json, 'settings', {}),
- tags: _.get(json, 'tags', []),
+ tags: _.get(json, 'meta.tags', []),
request: {
method: _.upperCase(_.get(json, 'http.method')),
url: _.get(json, 'http.url'),
diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js
index 319575ab9..9dd920d8d 100644
--- a/packages/bruno-electron/src/bru/index.js
+++ b/packages/bruno-electron/src/bru/index.js
@@ -138,6 +138,7 @@ const bruToJson = (data, parsed = false) => {
name: _.get(json, 'meta.name'),
seq: !_.isNaN(sequence) ? Number(sequence) : 1,
settings: _.get(json, 'settings', {}),
+ tags: _.get(json, 'meta.tags', []),
request: {
method: _.upperCase(_.get(json, 'http.method')),
url: _.get(json, 'http.url'),
@@ -149,14 +150,12 @@ const bruToJson = (data, parsed = false) => {
vars: _.get(json, 'vars', {}),
assertions: _.get(json, 'assertions', []),
tests: _.get(json, 'tests', ''),
- docs: _.get(json, 'docs', ''),
- tags: _.get(json, 'tags', [])
+ docs: _.get(json, 'docs', '')
}
};
transformedJson.request.auth.mode = _.get(json, 'http.auth', 'none');
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
-
return transformedJson;
} catch (e) {
return Promise.reject(e);
@@ -196,7 +195,8 @@ const jsonToBru = async (json) => {
meta: {
name: _.get(json, 'name'),
type: type,
- seq: !_.isNaN(sequence) ? Number(sequence) : 1
+ seq: !_.isNaN(sequence) ? Number(sequence) : 1,
+ tags: _.get(json, 'tags', []),
},
http: {
method: _.lowerCase(_.get(json, 'request.method')),
@@ -216,8 +216,7 @@ const jsonToBru = async (json) => {
assertions: _.get(json, 'request.assertions', []),
tests: _.get(json, 'request.tests', ''),
settings: _.get(json, 'settings', {}),
- docs: _.get(json, 'request.docs', ''),
- tags: _.get(json, 'request.tags', [])
+ docs: _.get(json, 'request.docs', '')
};
const bru = jsonToBruV2(bruJson);
@@ -239,7 +238,8 @@ const jsonToBruViaWorker = async (json) => {
meta: {
name: _.get(json, 'name'),
type: type,
- seq: !_.isNaN(sequence) ? Number(sequence) : 1
+ seq: !_.isNaN(sequence) ? Number(sequence) : 1,
+ tags: _.get(json, 'tags', [])
},
http: {
method: _.lowerCase(_.get(json, 'request.method')),
@@ -259,8 +259,7 @@ const jsonToBruViaWorker = async (json) => {
assertions: _.get(json, 'request.assertions', []),
tests: _.get(json, 'request.tests', ''),
settings: _.get(json, 'settings', {}),
- docs: _.get(json, 'request.docs', ''),
- tags: _.get(json, 'request.tags', [])
+ docs: _.get(json, 'request.docs', '')
};
const bru = await bruParserWorker?.jsonToBru(bruJson)
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index ef0750e7a..a61ba239c 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -1017,8 +1017,8 @@ const registerNetworkIpc = (mainWindow) => {
if (tags && tags.include && tags.exclude) {
const includeTags = tags.include ? tags.include : [];
const excludeTags = tags.exclude ? tags.exclude : [];
- folderRequests = folderRequests.filter(({ request }) => {
- return isRequestTagsIncluded(request.tags, includeTags, excludeTags)
+ folderRequests = folderRequests.filter(({ tags }) => {
+ return isRequestTagsIncluded(tags, includeTags, excludeTags)
});
}
diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js
index b12ac491a..3ce411841 100644
--- a/packages/bruno-lang/v2/src/bruToJson.js
+++ b/packages/bruno-lang/v2/src/bruToJson.js
@@ -52,7 +52,7 @@ const grammar = ohm.grammar(`Bru {
pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*
pair = st* key st* ":" st* value st*
key = keychar*
- value = multilinetextblock | valuechar*
+ value = list | multilinetextblock | valuechar*
// Dictionary for Assert Block
assertdictionary = st* "{" assertpairlist? tagend
@@ -67,12 +67,11 @@ const grammar = ohm.grammar(`Bru {
textchar = ~nl any
// List
- listend = nl "]"
+ listend = stnl* "]"
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*
- tags = "tags" list
meta = "meta" dictionary
settings = "settings" dictionary
@@ -276,6 +275,10 @@ const sem = grammar.createSemantics().addAttribute('ast', {
},
pair(_1, key, _2, _3, _4, value, _5) {
let res = {};
+ if (Array.isArray(value.ast)) {
+ res[key.ast] = value.ast;
+ return res;
+ }
res[key.ast] = value.ast ? value.ast.trim() : '';
return res;
},
@@ -283,6 +286,9 @@ const sem = grammar.createSemantics().addAttribute('ast', {
return chars.sourceString ? chars.sourceString.trim() : '';
},
value(chars) {
+ if (chars.ctorName === 'list') {
+ return chars.ast;
+ }
try {
let isMultiline = chars.sourceString?.startsWith(`'''`) && chars.sourceString?.endsWith(`'''`);
if (isMultiline) {
@@ -342,9 +348,6 @@ const sem = grammar.createSemantics().addAttribute('ast', {
_iter(...elements) {
return elements.map((e) => e.ast);
},
- tags(_1, list) {
- return { tags: list.ast };
- },
meta(_1, dictionary) {
let meta = mapPairListToKeyValPair(dictionary.ast);
diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js
index b4adfad92..3a072a254 100644
--- a/packages/bruno-lang/v2/src/jsonToBru.js
+++ b/packages/bruno-lang/v2/src/jsonToBru.js
@@ -36,18 +36,23 @@ const jsonToBru = (json) => {
if (meta) {
bru += 'meta {\n';
+
+ const tags = meta.tags;
+ delete meta.tags;
+
for (const key in meta) {
bru += ` ${key}: ${meta[key]}\n`;
}
- bru += '}\n\n';
- }
- if (tags) {
- bru += 'tags [\n';
- for (const tag of tags) {
- bru += ` ${tag}\n`;
+ if (tags && tags.length) {
+ bru += ` tags: [\n`;
+ for (const tag of tags) {
+ bru += ` ${tag}\n`;
+ }
+ bru += ` ]\n`;
}
- bru += ']\n\n';
+
+ bru += '}\n\n';
}
if (http && http.method) {
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru
index 5230a8132..dc70da54b 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.bru
+++ b/packages/bruno-lang/v2/tests/fixtures/request.bru
@@ -2,13 +2,12 @@ meta {
name: Send Bulk SMS
type: http
seq: 1
+ tags: [
+ foo
+ bar
+ ]
}
-tags [
- foo
- bar
-]
-
get {
url: https://api.textlocal.in/send/:id
body: json
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json
index b97419241..539eae116 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.json
+++ b/packages/bruno-lang/v2/tests/fixtures/request.json
@@ -2,9 +2,9 @@
"meta": {
"name": "Send Bulk SMS",
"type": "http",
- "seq": "1"
+ "seq": "1",
+ "tags": ["foo", "bar"]
},
- "tags": ["foo", "bar"],
"http": {
"method": "get",
"url": "https://api.textlocal.in/send/:id",
diff --git a/packages/bruno-lang/v2/tests/tags.spec.js b/packages/bruno-lang/v2/tests/tags.spec.js
new file mode 100644
index 000000000..58613bca8
--- /dev/null
+++ b/packages/bruno-lang/v2/tests/tags.spec.js
@@ -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);
+ });
+});
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index 185734bf6..2f143c0e1 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -310,8 +310,7 @@ const requestSchema = Yup.object({
.nullable(),
assertions: Yup.array().of(keyValueSchema).nullable(),
tests: Yup.string().nullable(),
- docs: Yup.string().nullable(),
- tags: Yup.array().of(Yup.string().matches(/^[\w-]+$/, 'tag must be alphanumeric'))
+ docs: Yup.string().nullable()
})
.noUnknown(true)
.strict();
@@ -356,6 +355,7 @@ const itemSchema = Yup.object({
type: Yup.string().oneOf(['http-request', 'graphql-request', 'folder', 'js']).required('type is required'),
seq: Yup.number().min(1),
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', {
is: (type) => ['http-request', 'graphql-request'].includes(type),
then: (schema) => schema.required('request is required when item-type is request')
diff --git a/packages/bruno-schema/src/collections/itemSchema.spec.js b/packages/bruno-schema/src/collections/itemSchema.spec.js
index 8c46bed2c..9d52132da 100644
--- a/packages/bruno-schema/src/collections/itemSchema.spec.js
+++ b/packages/bruno-schema/src/collections/itemSchema.spec.js
@@ -7,7 +7,8 @@ describe('Item Schema Validation', () => {
const item = {
uid: uuid(),
name: 'A Folder',
- type: 'folder'
+ type: 'folder',
+ tags: ['smoke-test']
};
const isValid = await itemSchema.validate(item);
diff --git a/packages/bruno-schema/src/collections/requestSchema.spec.js b/packages/bruno-schema/src/collections/requestSchema.spec.js
index 3e17e9190..9fd223cb2 100644
--- a/packages/bruno-schema/src/collections/requestSchema.spec.js
+++ b/packages/bruno-schema/src/collections/requestSchema.spec.js
@@ -9,7 +9,6 @@ describe('Request Schema Validation', () => {
method: 'GET',
headers: [],
params: [],
- tags: ['smoke-test'],
body: {
mode: 'none'
}