diff --git a/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/StyledWrapper.jsx b/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/StyledWrapper.jsx index 7597df13556..49ff7433646 100644 --- a/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/StyledWrapper.jsx +++ b/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/StyledWrapper.jsx @@ -1,7 +1,7 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` - background-color: ${(props) => props.theme.sidebar.bg}; + background-color: ${(props) => props.theme.bg}; height: 100%; display: flex; flex-direction: column; @@ -12,13 +12,14 @@ const StyledWrapper = styled.div` display: flex; align-items: center; justify-content: space-between; - padding: 1rem; - border-bottom: 1px solid ${(props) => props.theme.sidebar.dragbar}; - margin-bottom: 0.5rem; + padding: 0.75rem 1rem; + background-color: ${(props) => props.theme.background.mantle}; + border-bottom: 1px solid ${(props) => props.theme.border.border0}; .counter { - font-size: ${(props) => props.theme.font.size.base}; + font-size: ${(props) => props.theme.font.size.sm}; font-weight: 500; + color: ${(props) => props.theme.colors.text.subtext0}; } .actions { @@ -66,11 +67,12 @@ const StyledWrapper = styled.div` position: relative; height: 2.5rem; border: 1px solid transparent; - background-color: ${(props) => props.theme.sidebar.bg}; + background-color: ${(props) => props.theme.bg}; transition: transform 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease; &.is-selected { - background-color: ${(props) => props.theme.background.surface0}; + background-color: ${(props) => props.theme.background.mantle}; + border-color: ${(props) => props.theme.border.border0}; .checkbox { background-color: ${(props) => props.theme.primary.solid}; @@ -82,9 +84,32 @@ const StyledWrapper = styled.div` } } + &.is-disabled { + opacity: 0.4; + pointer-events: none; + user-select: none; + + .drag-handle { + visibility: hidden; + } + + .checkbox-container { + cursor: default; + + .checkbox { + border-color: ${(props) => props.theme.border.border2}; + background-color: ${(props) => props.theme.background.surface0}; + + &:hover { + border-color: ${(props) => props.theme.border.border2}; + } + } + } + } + &.is-dragging { opacity: 0.5; - background-color: ${(props) => props.theme.sidebar.bg}; + background-color: ${(props) => props.theme.bg}; border: 1px dashed ${(props) => props.theme.sidebar.dragbar}; transform: scale(0.98); box-shadow: ${(props) => props.theme.shadow.md}; diff --git a/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/index.jsx b/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/index.jsx index c25ae74c76e..4ccbb127838 100644 --- a/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/index.jsx +++ b/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/index.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useState, useCallback, useRef } from 'react'; import { useDrag, useDrop } from 'react-dnd'; import { getEmptyImage } from 'react-dnd-html5-backend'; -import { IconGripVertical, IconCheck, IconAdjustmentsAlt } from '@tabler/icons'; +import { IconGripVertical, IconCheck } from '@tabler/icons'; import { useDispatch } from 'react-redux'; import { updateRunnerConfiguration } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; @@ -9,6 +9,23 @@ import { isItemARequest } from 'utils/collections'; import path from 'utils/common/path'; import { cloneDeep, get } from 'lodash'; import Button from 'ui/Button/index'; +import { isRequestTagsIncluded } from '@usebruno/common'; + +const isRequestDisabled = (item, tags) => { + // WS and gRPC are not supported by the collection runner + if (item.type === 'ws-request' || item.type === 'grpc-request') return true; + + // Check tag filtering + const requestTags = item.draft?.tags || item.tags || []; + const includeTags = tags?.include || []; + const excludeTags = tags?.exclude || []; + + if (includeTags.length > 0 || excludeTags.length > 0) { + return !isRequestTagsIncluded(requestTags, includeTags, excludeTags); + } + + return false; +}; const ItemTypes = { REQUEST_ITEM: 'request-item' @@ -40,7 +57,7 @@ const getMethodInfo = (item) => { return { methodText, methodClass }; }; -const RequestItem = ({ item, index, moveItem, isSelected, onSelect, onDrop }) => { +const RequestItem = ({ item, index, moveItem, isSelected, onSelect, onDrop, isDisabled }) => { const ref = useRef(null); const [dropType, setDropType] = useState(null); @@ -58,6 +75,7 @@ const RequestItem = ({ item, index, moveItem, isSelected, onSelect, onDrop }) => const [{ isDragging }, drag, preview] = useDrag({ type: ItemTypes.REQUEST_ITEM, item: { uid: item.uid, name: item.name, request: item.request, index }, + canDrag: !isDisabled, collect: (monitor) => ({ isDragging: monitor.isDragging() }), options: { dropEffect: 'move' @@ -117,28 +135,30 @@ const RequestItem = ({ item, index, moveItem, isSelected, onSelect, onDrop }) => drag(drop(ref)); + const methodInfo = getMethodInfo(item); const itemClasses = [ 'request-item', isDragging ? 'is-dragging' : '', isSelected ? 'is-selected' : '', + isDisabled ? 'is-disabled' : '', isOver && canDrop && dropType === 'above' ? 'drop-target-above' : '', isOver && canDrop && dropType === 'below' ? 'drop-target-below' : '' ].filter(Boolean).join(' '); return ( -
+
-
onSelect(item)}> +
!isDisabled && onSelect(item)}>
- {isSelected && } + {isSelected && !isDisabled && }
-
- {getMethodInfo(item).methodText} +
+ {methodInfo.methodText}
@@ -151,11 +171,15 @@ const RequestItem = ({ item, index, moveItem, isSelected, onSelect, onDrop }) => ); }; -const RunConfigurationPanel = ({ collection, selectedItems, setSelectedItems }) => { +const RunConfigurationPanel = ({ collection, selectedItems, setSelectedItems, tags }) => { const dispatch = useDispatch(); const [flattenedRequests, setFlattenedRequests] = useState([]); const [originalRequests, setOriginalRequests] = useState([]); const [isLoading, setIsLoading] = useState(true); + // On first mount, ignore any stale saved config and auto-select all items + const isInitialMountRef = useRef(true); + // Track items that were auto-deselected due to tag filters, so we can re-select them when tags change back + const pendingReselectRef = useRef(new Set()); const flattenRequests = useCallback((collection) => { const result = []; @@ -192,6 +216,7 @@ const RunConfigurationPanel = ({ collection, selectedItems, setSelectedItems }) const requests = flattenRequests(structureCopy); const savedConfiguration = get(collection, 'runnerConfiguration', null); + let finalRequests; if (savedConfiguration?.requestItemsOrder?.length > 0) { const orderedRequests = []; const requestMap = new Map(requests.map((req) => [req.uid, req])); @@ -208,12 +233,21 @@ const RunConfigurationPanel = ({ collection, selectedItems, setSelectedItems }) orderedRequests.push(request); }); - setFlattenedRequests(orderedRequests); + finalRequests = orderedRequests; } else { - setFlattenedRequests(requests); + finalRequests = requests; } + setFlattenedRequests(finalRequests); setOriginalRequests(cloneDeep(requests)); + + if (!savedConfiguration || isInitialMountRef.current) { + isInitialMountRef.current = false; + const enabledUids = finalRequests + .filter((item) => !isRequestDisabled(item, tags)) + .map((item) => item.uid); + setSelectedItems(enabledUids); + } } catch (error) { console.error('Error loading collection structure:', error); } finally { @@ -221,6 +255,44 @@ const RunConfigurationPanel = ({ collection, selectedItems, setSelectedItems }) } }, [collection, flattenRequests]); + // When tags change: disable newly-filtered items, re-select previously-filtered items that are now enabled again + useEffect(() => { + if (flattenedRequests.length === 0) return; + + let newSelected = [...selectedItems]; + let changed = false; + + flattenedRequests.forEach((item) => { + const disabled = isRequestDisabled(item, tags); + const isCurrentlySelected = selectedItems.includes(item.uid); + const isPendingReselect = pendingReselectRef.current.has(item.uid); + + if (disabled && isCurrentlySelected) { + pendingReselectRef.current.add(item.uid); + newSelected = newSelected.filter((uid) => uid !== item.uid); + changed = true; + } else if (!disabled && isPendingReselect) { + pendingReselectRef.current.delete(item.uid); + if (!newSelected.includes(item.uid)) { + newSelected.push(item.uid); + changed = true; + } + } + }); + + if (changed) { + const ordered = flattenedRequests + .filter((r) => newSelected.includes(r.uid)) + .map((r) => r.uid); + setSelectedItems(ordered); + const allRequestUidsOrder = flattenedRequests.map((item) => item.uid); + dispatch(updateRunnerConfiguration(collection.uid, ordered, allRequestUidsOrder)); + } + }, [tags, flattenedRequests]); + + const enabledRequests = flattenedRequests.filter((item) => !isRequestDisabled(item, tags)); + const enabledCount = enabledRequests.length; + const moveItem = useCallback((draggedItemUid, hoverIndex) => { setFlattenedRequests((prevRequests) => { const dragIndex = prevRequests.findIndex((item) => item.uid === draggedItemUid); @@ -255,6 +327,8 @@ const RunConfigurationPanel = ({ collection, selectedItems, setSelectedItems }) }, [selectedItems, collection.uid, dispatch, setSelectedItems]); const handleRequestSelect = useCallback((item) => { + if (isRequestDisabled(item, tags)) return; + try { if (selectedItems.includes(item.uid)) { const newSelectedUids = selectedItems.filter((uid) => uid !== item.uid); @@ -277,51 +351,61 @@ const RunConfigurationPanel = ({ collection, selectedItems, setSelectedItems }) } catch (error) { console.error('Error selecting item:', error); } - }, [selectedItems, setSelectedItems, flattenedRequests, dispatch, collection.uid]); + }, [selectedItems, setSelectedItems, flattenedRequests, dispatch, collection.uid, tags]); const handleSelectAll = useCallback(() => { try { const allRequestUidsOrder = flattenedRequests.map((item) => item.uid); + const enabledUids = enabledRequests.map((item) => item.uid); - if (selectedItems.length === flattenedRequests.length) { + if (selectedItems.length === enabledCount) { + pendingReselectRef.current.clear(); setSelectedItems([]); dispatch(updateRunnerConfiguration(collection.uid, [], allRequestUidsOrder)); } else { - setSelectedItems(allRequestUidsOrder); - dispatch(updateRunnerConfiguration(collection.uid, allRequestUidsOrder, allRequestUidsOrder)); + setSelectedItems(enabledUids); + dispatch(updateRunnerConfiguration(collection.uid, enabledUids, allRequestUidsOrder)); } } catch (error) { console.error('Error selecting/deselecting all items:', error); } - }, [flattenedRequests, selectedItems, setSelectedItems, dispatch, collection.uid]); + }, [flattenedRequests, enabledRequests, enabledCount, selectedItems, setSelectedItems, dispatch, collection.uid]); const handleReset = useCallback(() => { try { - setFlattenedRequests(cloneDeep(originalRequests)); - setSelectedItems([]); - dispatch(updateRunnerConfiguration(collection.uid, [], [])); + pendingReselectRef.current.clear(); + const resetRequests = cloneDeep(originalRequests); + setFlattenedRequests(resetRequests); + const enabledUids = resetRequests + .filter((item) => !isRequestDisabled(item, tags)) + .map((item) => item.uid); + setSelectedItems(enabledUids); + const allUidsOrder = resetRequests.map((item) => item.uid); + dispatch(updateRunnerConfiguration(collection.uid, enabledUids, allUidsOrder)); } catch (error) { console.error('Error resetting configuration:', error); } - }, [originalRequests, setSelectedItems, collection.uid, dispatch]); + }, [originalRequests, setSelectedItems, collection.uid, dispatch, tags]); return ( - +
-
- {selectedItems.length} of {flattenedRequests.length} selected +
+ {selectedItems.length} of {enabledCount} selected
@@ -337,6 +421,7 @@ const RunConfigurationPanel = ({ collection, selectedItems, setSelectedItems })
{flattenedRequests.map((item, idx) => { const isSelected = selectedItems.includes(item.uid); + const disabled = isRequestDisabled(item, tags); return ( handleRequestSelect(item)} moveItem={moveItem} onDrop={handleDrop} diff --git a/packages/bruno-app/src/components/RunnerResults/RunnerTags/index.jsx b/packages/bruno-app/src/components/RunnerResults/RunnerTags/index.jsx index 959169d4fa4..2118e09ede7 100644 --- a/packages/bruno-app/src/components/RunnerResults/RunnerTags/index.jsx +++ b/packages/bruno-app/src/components/RunnerResults/RunnerTags/index.jsx @@ -9,13 +9,7 @@ const RunnerTags = ({ collectionUid, className = '' }) => { 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)); @@ -39,12 +33,9 @@ const RunnerTags = ({ collectionUid, className = '' }) => { 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; - } + if (!availableTags.includes(trimmedTag)) return; const newTags = { ...tags, include: [...tags.include, trimmedTag].sort() }; setTags(newTags); return; @@ -52,9 +43,7 @@ const RunnerTags = ({ collectionUid, className = '' }) => { // add tag to the `exclude` list if (to === 'exclude') { if (tags.include.includes(trimmedTag) || tags.exclude.includes(trimmedTag)) return; - if (!availableTags.includes(trimmedTag)) { - return; - } + if (!availableTags.includes(trimmedTag)) return; const newTags = { ...tags, exclude: [...tags.exclude, trimmedTag].sort() }; setTags(newTags); } @@ -82,47 +71,30 @@ const RunnerTags = ({ collectionUid, className = '' }) => { dispatch(updateRunnerTagsDetails({ collectionUid: collection.uid, tags })); }; - const setTagsEnabled = (tagsEnabled) => { - dispatch(updateRunnerTagsDetails({ collectionUid: collection.uid, tagsEnabled })); - }; - return ( -
-
- setTagsEnabled(!tagsEnabled)} - /> - -
- {tagsEnabled && ( -
-
- Included tags: - handleAddTag({ tag, to: 'include' })} - handleRemoveTag={(tag) => handleRemoveTag({ tag, from: 'include' })} - tagsHintList={tagsHintList} - handleValidation={handleValidation} - /> -
-
- Excluded tags: - handleAddTag({ tag, to: 'exclude' })} - handleRemoveTag={(tag) => handleRemoveTag({ tag, from: 'exclude' })} - tagsHintList={tagsHintList} - handleValidation={handleValidation} - /> -
+
+
+
+ Include tags + handleAddTag({ tag, to: 'include' })} + handleRemoveTag={(tag) => handleRemoveTag({ tag, from: 'include' })} + tagsHintList={tagsHintList} + handleValidation={handleValidation} + /> +
+
+ Exclude tags + handleAddTag({ tag, to: 'exclude' })} + handleRemoveTag={(tag) => handleRemoveTag({ tag, from: 'exclude' })} + tagsHintList={tagsHintList} + handleValidation={handleValidation} + />
- )} +
); }; diff --git a/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js b/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js index b9750b23bcd..18fb3ecf09f 100644 --- a/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js +++ b/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js @@ -3,18 +3,48 @@ import styled from 'styled-components'; const Wrapper = styled.div` .textbox { padding: 0.2rem 0.5rem; - box-shadow: none; - border-radius: 0px; outline: none; - box-shadow: none; - transition: border-color ease-in-out 0.1s; - border-radius: 3px; + font-size: ${(props) => props.theme.font.size.sm}; + border-radius: ${(props) => props.theme.border.radius.sm}; background-color: ${(props) => props.theme.input.bg}; border: 1px solid ${(props) => props.theme.input.border}; + height: 1.875rem; &:focus { - border: solid 1px ${(props) => props.theme.input.focusBorder} !important; - outline: none !important; + outline: none; + border-color: ${(props) => props.theme.input.focusBorder}; + } + + &[type='number'] { + -moz-appearance: textfield; + appearance: textfield; + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + } + } + + /* Radio button styles */ + input[type='radio'] { + cursor: pointer; + appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + border: 1px solid ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.bg}; + flex-shrink: 0; + + &:focus-visible { + outline: 2px solid ${(props) => props.theme.input.focusBorder}; + outline-offset: 2px; + } + + &:checked { + border: 1px solid ${(props) => props.theme.primary.solid}; + background-image: radial-gradient(circle, ${(props) => props.theme.primary.solid} 40%, ${(props) => props.theme.bg} 42%); } } @@ -78,6 +108,43 @@ const Wrapper = styled.div` border-color: ${(props) => props.theme.background.surface1}; } + .runner-section-title { + font-size: ${(props) => props.theme.font.size.sm}; + font-weight: 600; + } + + .runner-section { + font-size: ${(props) => props.theme.font.size.sm}; + + div:has(> .single-line-editor) { + height: 1.875rem; + border: 1px solid ${(props) => props.theme.input.border}; + border-radius: ${(props) => props.theme.border.radius.sm}; + background-color: ${(props) => props.theme.input.bg}; + padding: 0.2rem 0.5rem; + } + + div:has(> .single-line-editor):focus-within { + border-color: ${(props) => props.theme.input.focusBorder}; + } + + .single-line-editor { + height: 1.475rem; + font-size: ${(props) => props.theme.font.size.sm}; + + .CodeMirror { + height: 1.475rem; + line-height: 1.475rem; + } + + .CodeMirror-cursor { + height: 0.875rem !important; + margin-top: 0.3rem !important; + } + } + } + + .filter-bar { display: flex; align-items: stretch; diff --git a/packages/bruno-app/src/components/RunnerResults/index.jsx b/packages/bruno-app/src/components/RunnerResults/index.jsx index 62255514560..f915ed1cf54 100644 --- a/packages/bruno-app/src/components/RunnerResults/index.jsx +++ b/packages/bruno-app/src/components/RunnerResults/index.jsx @@ -3,8 +3,8 @@ import path from 'utils/common/path'; import { useDispatch } from 'react-redux'; import { get, cloneDeep } from 'lodash'; import { runCollectionFolder, cancelRunnerExecution, mountCollection, updateRunnerConfiguration } from 'providers/ReduxStore/slices/collections/actions'; -import { resetCollectionRunner, updateRunnerTagsDetails } from 'providers/ReduxStore/slices/collections'; -import { findItemInCollection, getTotalRequestCountInCollection, areItemsLoading, getRequestItemsForCollectionRun } from 'utils/collections'; +import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections'; +import { findItemInCollection, getTotalRequestCountInCollection, areItemsLoading } from 'utils/collections'; import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun, IconExternalLink } from '@tabler/icons'; import ResponsePane from './ResponsePane'; import StyledWrapper from './StyledWrapper'; @@ -81,7 +81,6 @@ export default function RunnerResults({ collection }) { const [delay, setDelay] = useState(null); const [activeFilter, setActiveFilter] = useState('all'); const [selectedRequestItems, setSelectedRequestItems] = useState([]); - const [configureMode, setConfigureMode] = useState(false); // ref for the runner output body const runnerBodyRef = useRef(); @@ -91,16 +90,9 @@ export default function RunnerResults({ collection }) { // 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 requestItemsForCollectionRun = getRequestItemsForCollectionRun({ recursive: true, tags, items: collection.items }); - const totalRequestItemsCountForCollectionRun = requestItemsForCollectionRun.length; - const shouldDisableCollectionRun = totalRequestItemsCountForCollectionRun <= 0; - const items = cloneDeep(get(collection, 'runnerResult.items', [])) .map((item) => { const info = findItemInCollection(collectionCopy, item.uid); @@ -164,24 +156,14 @@ export default function RunnerResults({ collection }) { } }, [filteredItems]); - useEffect(() => { - const runnerInfo = get(collection, 'runnerResult.info', {}); - if (runnerInfo.status === 'running') { - setConfigureMode(false); - } - }, [collection.runnerResult]); - useEffect(() => { const savedConfiguration = get(collection, 'runnerConfiguration', null); if (savedConfiguration) { - if (savedConfiguration.selectedRequestItems && configureMode) { - setSelectedRequestItems(savedConfiguration.selectedRequestItems); - } if (savedConfiguration.delay !== undefined && delay === null) { setDelay(savedConfiguration.delay); } } - }, [collection.runnerConfiguration, configureMode, delay]); + }, [collection.runnerConfiguration, delay]); const ensureCollectionIsMounted = () => { if (collection.mountStatus === 'mounted') { @@ -195,13 +177,9 @@ export default function RunnerResults({ collection }) { }; const runCollection = () => { - if (configureMode && selectedRequestItems.length > 0) { - dispatch(updateRunnerConfiguration(collection.uid, selectedRequestItems, selectedRequestItems, delay)); - dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags, selectedRequestItems)); - } else { - dispatch(updateRunnerConfiguration(collection.uid, [], [], delay)); - dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags)); - } + const savedOrder = get(collection, 'runnerConfiguration.requestItemsOrder', selectedRequestItems); + dispatch(updateRunnerConfiguration(collection.uid, selectedRequestItems, savedOrder, delay)); + dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tags, selectedRequestItems)); }; const runAgain = () => { @@ -216,7 +194,7 @@ export default function RunnerResults({ collection }) { runnerInfo.folderUid, true, Number(savedDelay), - tagsEnabled && tags, + tags, savedSelectedItems ) ); @@ -228,8 +206,6 @@ export default function RunnerResults({ collection }) { collectionUid: collection.uid }) ); - setSelectedRequestItems([]); - setConfigureMode(false); setDelay(null); }; @@ -237,17 +213,6 @@ export default function RunnerResults({ collection }) { dispatch(cancelRunnerExecution(runnerInfo.cancelTokenUid)); }; - const toggleConfigureMode = () => { - dispatch(updateRunnerTagsDetails({ collectionUid: collection.uid, tagsEnabled: false })); - setConfigureMode(!configureMode); - }; - - useEffect(() => { - if (tagsEnabled) { - setConfigureMode(false); - } - }, [tagsEnabled]); - const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy); const filterCounts = { all: items.length, @@ -261,13 +226,13 @@ export default function RunnerResults({ collection }) { return (
-
+
+ Runner -
-
- You have {totalRequestsInCollection} requests in this collection. +
+ You have {totalRequestsInCollection} {totalRequestsInCollection === 1 ? 'request' : 'requests'} in this collection. {isCollectionLoading && ( (Loading...) @@ -275,47 +240,40 @@ export default function RunnerResults({ collection }) { )}
{isCollectionLoading ?
Requests in this collection are still loading.
: null} -
- + + {/* Timings */} +
Timings
+
+ setDelay(e.target.value)} />
- {/* Tags for the collection run */} - - - {/* Configure requests option */} -
-
- - -
+ {/* Filters */} +
Filters
+
+ {/* Tags for the collection run */} +
- {configureMode && ( -
- -
- )} +
+ +
); @@ -399,7 +356,7 @@ export default function RunnerResults({ collection }) {
- {tagsEnabled && areTagsAdded && ( + {areTagsAdded && (
Tags:
@@ -457,7 +414,7 @@ export default function RunnerResults({ collection }) { )}
- {tagsEnabled && areTagsAdded && item?.tags?.length > 0 && ( + {areTagsAdded && item?.tags?.length > 0 && (
Tags: {item.tags.filter((t) => tags.include.includes(t)).join(', ')}
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js index e7dd94d2fc2..0f308b0baed 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js @@ -4,9 +4,93 @@ const Wrapper = styled.div` .bruno-modal-content { padding-bottom: 1rem; } + + .description { + color: ${(props) => props.theme.colors.text.muted}; + } + + .divider { + border: none; + border-top: 1px solid ${(props) => props.theme.input.border}; + margin: 1rem 0rem; + } + .warning { color: ${(props) => props.theme.colors.text.danger}; } + + .textbox { + padding: 0.2rem 0.5rem; + outline: none; + font-size: ${(props) => props.theme.font.size.sm}; + border-radius: ${(props) => props.theme.border.radius.sm}; + background-color: ${(props) => props.theme.input.bg}; + border: 1px solid ${(props) => props.theme.input.border}; + height: 1.875rem; + + &:focus { + outline: none; + border-color: ${(props) => props.theme.input.focusBorder}; + } + + &[type='number'] { + -moz-appearance: textfield; + appearance: textfield; + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + } + } + + div:has(> .single-line-editor) { + height: 1.875rem; + border: 1px solid ${(props) => props.theme.input.border}; + border-radius: ${(props) => props.theme.border.radius.sm}; + background-color: ${(props) => props.theme.input.bg}; + padding: 0.2rem 0.5rem; + } + + div:has(> .single-line-editor):focus-within { + border-color: ${(props) => props.theme.input.focusBorder}; + } + + .single-line-editor { + height: 1.475rem; + font-size: ${(props) => props.theme.font.size.sm}; + + .CodeMirror { + height: 1.475rem; + line-height: 1.475rem; + } + + .CodeMirror-cursor { + height: 0.875rem !important; + margin-top: 0.3rem !important; + } + } + + input[type='radio'] { + cursor: pointer; + appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + border: 1px solid ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.bg}; + flex-shrink: 0; + + &:focus-visible { + outline: 2px solid ${(props) => props.theme.input.focusBorder}; + outline-offset: 2px; + } + + &:checked { + border: 1px solid ${(props) => props.theme.primary.solid}; + background-image: radial-gradient(circle, ${(props) => props.theme.primary.solid} 40%, ${(props) => props.theme.bg} 42%); + } + } `; export default Wrapper; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js index af0e0eea836..a96afcaae91 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import get from 'lodash/get'; import { uuid } from 'utils/common'; import Modal from 'components/Modal'; @@ -14,6 +14,7 @@ import Button from 'ui/Button'; const RunCollectionItem = ({ collectionUid, item, onClose }) => { const dispatch = useDispatch(); + const [delay, setDelay] = useState(''); const collection = useSelector((state) => state.collections.collections?.find((c) => c.uid === collectionUid)); const isCollectionRunInProgress = collection?.runnerResult?.info?.status && (collection?.runnerResult?.info?.status !== 'ended'); @@ -21,9 +22,6 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => { // tags for the collection run const tags = get(collection, 'runnerTags', { include: [], exclude: [] }); - // have tags been enabled for the collection run - const tagsEnabled = get(collection, 'runnerTagsEnabled', false); - const onSubmit = (recursive) => { dispatch( addTab({ @@ -33,7 +31,7 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => { }) ); if (!isCollectionRunInProgress) { - dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive, 0, tagsEnabled && tags)); + dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive, delay ? Number(delay) : null, tags)); } onClose(); }; @@ -68,15 +66,34 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => { Run ({totalRequestItemsCountForFolderRun} requests)
-
This will only run the requests in this folder.
+
This will only run the requests in this folder.
Recursive Run ({totalRequestItemsCountForRecursiveFolderRun} requests)
-
This will run all the requests in this folder and all its subfolders.
+
This will run all the requests in this folder and all its subfolders.
{isFolderLoading ?
Requests in this folder are still loading.
: null} {isCollectionRunInProgress ?
A Collection Run is already in progress.
: null} +
+ + {/* Timings */} +
+ + setDelay(e.target.value)} + /> +
+ {/* Tags for the collection run */} diff --git a/packages/bruno-app/src/components/TagList/StyledWrapper.js b/packages/bruno-app/src/components/TagList/StyledWrapper.js index efb5daacd06..603a716c6ab 100644 --- a/packages/bruno-app/src/components/TagList/StyledWrapper.js +++ b/packages/bruno-app/src/components/TagList/StyledWrapper.js @@ -66,14 +66,12 @@ const StyledWrapper = styled.div` opacity: 0.7; &:hover { - background-color: ${(props) => props.theme.danger}; - color: white; + color: ${(props) => props.theme.text}; opacity: 1; - transform: scale(1.1); } &:focus-visible { - outline: 2px solid ${(props) => props.theme.danger}; + outline: 2px solid ${(props) => props.theme.text}; outline-offset: 1px; } } diff --git a/packages/bruno-app/src/components/TagList/index.js b/packages/bruno-app/src/components/TagList/index.js index b35af1743a5..4317116dd17 100644 --- a/packages/bruno-app/src/components/TagList/index.js +++ b/packages/bruno-app/src/components/TagList/index.js @@ -47,7 +47,7 @@ const TagList = ({ tagsHintList = [], handleAddTag, tags, handleRemoveTag, onSav { // Wait for the runner tab to open // If there are existing results, reset first, otherwise wait for Run Collection button const resetButton = page.getByRole('button', { name: 'Reset' }); - const runCollectionButton = page.getByRole('button', { name: 'Run Collection' }); + const runCollectionButton = page.getByTestId('runner-run-button'); // Check if Reset button is visible (means there are existing results) const resetVisible = await resetButton.isVisible().catch(() => false); diff --git a/tests/runner/runner-configuration.spec.ts b/tests/runner/runner-configuration.spec.ts new file mode 100644 index 00000000000..b80612c2e3c --- /dev/null +++ b/tests/runner/runner-configuration.spec.ts @@ -0,0 +1,156 @@ +import { test, expect } from '../../playwright'; +import { openRunnerTab, buildRunnerLocators } from '../utils/page/index'; + +const COLLECTION_NAME = 'bruno-testbench'; + +/** + * Waits for the config panel to finish loading and initializing requests. + * On first load, all enabled requests are auto-selected, so we wait until selected === total. + */ +const waitForRequestsInitialized = async (locators) => { + await expect(async () => { + const text = await locators.configCounter().innerText(); + const match = text.match(/(\d+) of (\d+) selected/); + expect(match).toBeTruthy(); + const selected = parseInt(match![1]); + const total = parseInt(match![2]); + expect(total).toBeGreaterThan(0); + expect(selected).toBe(total); + }).toPass({ timeout: 30000 }); +}; + +test.describe('Runner Configuration Panel', () => { + test('should display config panel with all requests selected by default', async ({ pageWithUserData: page }) => { + const locators = buildRunnerLocators(page); + await openRunnerTab(page, COLLECTION_NAME); + await waitForRequestsInitialized(locators); + + await test.step('Config panel is visible with request items', async () => { + await expect(locators.configPanel()).toBeVisible(); + const itemCount = await locators.requestItems().count(); + expect(itemCount).toBeGreaterThan(0); + }); + + await test.step('Counter shows all enabled requests selected', async () => { + const counterText = await locators.configCounter().innerText(); + const match = counterText.match(/(\d+) of (\d+) selected/); + expect(match).toBeTruthy(); + expect(match![1]).toBe(match![2]); + }); + + await test.step('Select All button shows "Deselect All" when all selected', async () => { + await expect(locators.selectAllButton()).toContainText('Deselect All'); + }); + }); + + test('should toggle select all / deselect all', async ({ pageWithUserData: page }) => { + const locators = buildRunnerLocators(page); + await openRunnerTab(page, COLLECTION_NAME); + await waitForRequestsInitialized(locators); + + await test.step('Click Deselect All', async () => { + await locators.selectAllButton().click(); + await expect(locators.selectAllButton()).toContainText('Select All', { timeout: 10000 }); + await expect(async () => { + const counterText = await locators.configCounter().innerText(); + expect(counterText).toMatch(/^0 of \d+ selected$/); + }).toPass({ timeout: 5000 }); + }); + + await test.step('Click Select All', async () => { + await locators.selectAllButton().click(); + await expect(locators.selectAllButton()).toContainText('Deselect All', { timeout: 10000 }); + }); + }); + + test('should deselect individual request items', async ({ pageWithUserData: page }) => { + const locators = buildRunnerLocators(page); + await openRunnerTab(page, COLLECTION_NAME); + await waitForRequestsInitialized(locators); + + await test.step('Deselect first item and verify count decreases', async () => { + const counterText = await locators.configCounter().innerText(); + const match = counterText.match(/(\d+) of (\d+) selected/); + expect(match).toBeTruthy(); + const initialSelected = parseInt(match![1]); + expect(initialSelected).toBeGreaterThan(1); + + // Click the checkbox area of the first request item to deselect it + const firstItem = locators.requestItems().first(); + await firstItem.locator('.checkbox-container').click(); + + // Verify count decreased by 1 + await expect(async () => { + const newCounterText = await locators.configCounter().innerText(); + const newMatch = newCounterText.match(/(\d+) of (\d+) selected/); + expect(parseInt(newMatch![1])).toBe(initialSelected - 1); + }).toPass({ timeout: 5000 }); + }); + + await test.step('Re-select item to restore state', async () => { + const firstItem = locators.requestItems().first(); + await firstItem.locator('.checkbox-container').click(); + await waitForRequestsInitialized(locators); + }); + }); + + test('should set delay value', async ({ pageWithUserData: page }) => { + const locators = buildRunnerLocators(page); + await openRunnerTab(page, COLLECTION_NAME); + + await test.step('Enter delay value', async () => { + const delayInput = locators.delayInput(); + await expect(delayInput).toBeVisible(); + await delayInput.fill('500'); + await expect(delayInput).toHaveValue('500'); + }); + }); + + test('should reset config panel to defaults', async ({ pageWithUserData: page }) => { + const locators = buildRunnerLocators(page); + await openRunnerTab(page, COLLECTION_NAME); + await waitForRequestsInitialized(locators); + + const counterText = await locators.configCounter().innerText(); + const initialMatch = counterText.match(/(\d+) of (\d+) selected/); + const totalEnabled = parseInt(initialMatch![2]); + + await test.step('Deselect all items first', async () => { + await locators.selectAllButton().click(); + await expect(locators.selectAllButton()).toContainText('Select All', { timeout: 10000 }); + }); + + await test.step('Click config reset to restore defaults', async () => { + await locators.configResetButton().click(); + + // After reset, all enabled items should be re-selected + await expect(async () => { + const text = await locators.configCounter().innerText(); + const match = text.match(/(\d+) of (\d+) selected/); + expect(match).toBeTruthy(); + expect(parseInt(match![1])).toBe(totalEnabled); + }).toPass({ timeout: 5000 }); + await expect(locators.selectAllButton()).toContainText('Deselect All'); + }); + }); + + test('should disable run button when no requests selected', async ({ pageWithUserData: page }) => { + const locators = buildRunnerLocators(page); + await openRunnerTab(page, COLLECTION_NAME); + await waitForRequestsInitialized(locators); + + await test.step('Deselect all and check run button is disabled', async () => { + await locators.selectAllButton().click(); + await expect(locators.selectAllButton()).toContainText('Select All', { timeout: 10000 }); + const runButton = page.locator('button[type="submit"]'); + await expect(runButton).toBeDisabled({ timeout: 10000 }); + }); + + await test.step('Select all and check run button is enabled', async () => { + await locators.selectAllButton().click(); + await expect(locators.selectAllButton()).toContainText('Deselect All', { timeout: 10000 }); + const runButton = page.locator('button[type="submit"]'); + await expect(runButton).toBeEnabled({ timeout: 10000 }); + }); + }); +}); diff --git a/tests/utils/page/runner.ts b/tests/utils/page/runner.ts index 3131d70b995..edeb762102f 100644 --- a/tests/utils/page/runner.ts +++ b/tests/utils/page/runner.ts @@ -12,8 +12,14 @@ export const buildRunnerLocators = (page: Page) => ({ failedButton: () => page.locator('button').filter({ hasText: /^Failed/ }), skippedButton: () => page.locator('button').filter({ hasText: /^Skipped/ }), resetButton: () => page.getByRole('button', { name: 'Reset' }), - runCollectionButton: () => page.getByRole('button', { name: 'Run Collection' }), - runAgainButton: () => page.getByRole('button', { name: 'Run Again' }) + runCollectionButton: () => page.getByTestId('runner-run-button'), + runAgainButton: () => page.getByRole('button', { name: 'Run Again' }), + configPanel: () => page.getByTestId('runner-config-panel'), + configCounter: () => page.getByTestId('runner-config-counter'), + selectAllButton: () => page.getByTestId('runner-select-all'), + configResetButton: () => page.getByTestId('runner-config-reset'), + requestItems: () => page.getByTestId('runner-request-item'), + delayInput: () => page.getByTestId('runner-delay-input') }); /** @@ -32,6 +38,35 @@ export const getRunnerResultCounts = async (page: Page) => { return { totalRequests, passed, failed, skipped }; }; +/** + * Opens the runner tab for a collection without starting a run + * @param page - The Playwright page object + * @param collectionName - The name of the collection to open the runner for + * @returns void + */ +export const openRunnerTab = async (page: Page, collectionName: string) => { + await test.step(`Open runner tab for "${collectionName}"`, async () => { + const collectionContainer = page.getByTestId('collections').locator('.collection-name').filter({ hasText: collectionName }); + await collectionContainer.waitFor({ state: 'visible' }); + + const actionsContainer = collectionContainer.locator('.collection-actions'); + await collectionContainer.hover(); + await actionsContainer.waitFor({ state: 'visible' }); + + const icon = actionsContainer.locator('.icon'); + await icon.waitFor({ state: 'visible', timeout: 5000 }); + await icon.click(); + + const runMenuItem = page.getByText('Run', { exact: true }); + await runMenuItem.waitFor({ state: 'visible' }); + await runMenuItem.click(); + + // Wait for the config panel to load + const locators = buildRunnerLocators(page); + await locators.configPanel().waitFor({ state: 'visible', timeout: 10000 }); + }); +}; + /** * Runs a collection by clicking the Run menu item and handling the runner tab * Includes logic to reset existing results if present