From 5cdb8ae0a14d09d8c5796e30c3b44f3e62add517 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 <54320162+Pragadesh-45@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:16:51 +0530 Subject: [PATCH] feat: add search functionality to environment variables. (#6659) --- .../EnvironmentVariables/index.js | 58 ++++++++++++------ .../EnvironmentDetails/StyledWrapper.js | 55 +++++++++++++++++ .../EnvironmentDetails/index.js | 35 ++++++++++- .../EnvironmentVariables/index.js | 61 +++++++++++++------ .../EnvironmentDetails/StyledWrapper.js | 60 +++++++++++++++++- .../EnvironmentDetails/index.js | 35 ++++++++++- 6 files changed, 262 insertions(+), 42 deletions(-) diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index dee8e1e87..a83ff0b34 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -19,15 +19,15 @@ import { getGlobalEnvironmentVariables, flattenItems, isItemARequest } from 'uti import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; import { sensitiveFields } from './constants'; -const TableRow = React.memo(({ children, item }) => {children}, (prevProps, nextProps) => { - const prevUid = prevProps?.item?.uid; - const nextUid = nextProps?.item?.uid; +const TableRow = React.memo(({ children, item }) => {children}, (prevProps, nextProps) => { + const prevUid = prevProps?.item?.variable?.uid; + const nextUid = nextProps?.item?.variable?.uid; return prevUid === nextUid && prevProps.children === nextProps.children; }); const MIN_H = 35 * 2; // 2 rows worth of height -const EnvironmentVariables = ({ environment, setIsModified, collection }) => { +const EnvironmentVariables = ({ environment, setIsModified, collection, searchQuery = '' }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments); @@ -397,13 +397,35 @@ const EnvironmentVariables = ({ environment, setIsModified, collection }) => { }; }, []); + const filteredVariables = useMemo(() => { + const allVariables = formik.values.map((variable, index) => ({ variable, index })); + if (!searchQuery?.trim()) { + return allVariables; + } + + const query = searchQuery.toLowerCase().trim(); + + return allVariables.filter(({ variable, index }) => { + const isLastRow = index === formik.values.length - 1; + const isEmptyRow = !variable.name || variable.name.trim() === ''; + if (isLastRow && isEmptyRow) { + return true; + } + + const nameMatch = variable.name ? variable.name.toLowerCase().includes(query) : false; + const valueMatch = typeof variable.value === 'string' ? variable.value.toLowerCase().includes(query) : false; + + return !!(nameMatch || valueMatch); + }); + }, [formik.values, searchQuery]); + return ( ( @@ -415,9 +437,9 @@ const EnvironmentVariables = ({ environment, setIsModified, collection }) => { )} fixedItemHeight={35} - computeItemKey={(index, variable) => variable.uid} - itemContent={(index, variable) => { - const isLastRow = index === formik.values.length - 1; + computeItemKey={(index, item) => item.variable.uid} + itemContent={(index, { variable, index: actualIndex }) => { + const isLastRow = actualIndex === formik.values.length - 1; const isEmptyRow = !variable.name || variable.name.trim() === ''; const isLastEmptyRow = isLastRow && isEmptyRow; @@ -428,7 +450,7 @@ const EnvironmentVariables = ({ environment, setIsModified, collection }) => { @@ -443,15 +465,15 @@ const EnvironmentVariables = ({ environment, setIsModified, collection }) => { autoCapitalize="off" spellCheck="false" className="mousetrap" - id={`${index}.name`} - name={`${index}.name`} + id={`${actualIndex}.name`} + name={`${actualIndex}.name`} value={variable.name} placeholder={isLastEmptyRow ? 'Name' : ''} - onChange={(e) => handleNameChange(index, e)} - onBlur={() => handleNameBlur(index)} - onKeyDown={(e) => handleNameKeyDown(index, e)} + onChange={(e) => handleNameChange(actualIndex, e)} + onBlur={() => handleNameBlur(actualIndex)} + onKeyDown={(e) => handleNameKeyDown(actualIndex, e)} /> - + @@ -459,12 +481,12 @@ const EnvironmentVariables = ({ environment, setIsModified, collection }) => { formik.setFieldValue(`${index}.value`, newValue, true)} + onChange={(newValue) => formik.setFieldValue(`${actualIndex}.value`, newValue, true)} onSave={handleSave} /> @@ -490,7 +512,7 @@ const EnvironmentVariables = ({ environment, setIsModified, collection }) => { diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/StyledWrapper.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/StyledWrapper.js index f879a90d7..b6b199f77 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/StyledWrapper.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/StyledWrapper.js @@ -94,8 +94,63 @@ const StyledWrapper = styled.div` .actions { display: flex; + align-items: center; gap: 2px; + .search-container { + position: relative; + top: 6px; + display: flex; + align-items: center; + + .search-icon { + position: absolute; + color: ${(props) => props.theme.colors.text.muted}; + pointer-events: none; + } + + .search-input { + width: 200px; + padding: 5px 32px 5px 32px; + border: 1px solid ${(props) => props.theme.input.border}; + border-radius: ${(props) => props.theme.border.radius.sm}; + background: ${(props) => props.theme.input.bg}; + color: ${(props) => props.theme.text}; + font-size: ${(props) => props.theme.font.size.base}; + outline: none; + transition: border-color 0.15s ease; + + &:focus { + border-color: ${(props) => props.theme.input.focusBorder}; + } + + &::placeholder { + color: ${(props) => props.theme.input.placeholder.color}; + opacity: ${(props) => props.theme.input.placeholder.opacity}; + } + } + + .clear-search { + position: absolute; + right: 8px; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; + color: ${(props) => props.theme.colors.text.muted}; + background: transparent; + border: none; + cursor: pointer; + border-radius: ${(props) => props.theme.border.radius.sm}; + transition: all 0.15s ease; + + &:hover { + color: ${(props) => props.theme.text}; + background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + } + } + } + button { display: inline-flex; align-items: center; diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js index eaf092f14..d7adcc81b 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js @@ -1,6 +1,7 @@ -import { IconCopy, IconEdit, IconTrash, IconCheck, IconX } from '@tabler/icons'; +import { IconCopy, IconEdit, IconTrash, IconCheck, IconX, IconSearch } from '@tabler/icons'; import { useState, useRef } from 'react'; import { useDispatch } from 'react-redux'; +import useDebounce from 'hooks/useDebounce'; import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions'; import { validateName, validateNameError } from 'utils/common/regex'; import toast from 'react-hot-toast'; @@ -18,6 +19,8 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => { const [isRenaming, setIsRenaming] = useState(false); const [newName, setNewName] = useState(''); const [nameError, setNameError] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const debouncedSearchQuery = useDebounce(searchQuery, 300); const inputRef = useRef(null); const validateEnvironmentName = (name) => { @@ -162,6 +165,29 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => { {nameError && isRenaming &&
{nameError}
}
+
+ + setSearchQuery(e.target.value)} + className="search-input" + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + spellCheck="false" + /> + {searchQuery && ( + + )} +
@@ -175,7 +201,12 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
- +
); diff --git a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index 73a741948..a27a17f39 100644 --- a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -1,4 +1,4 @@ -import React, { useCallback, useRef } from 'react'; +import React, { useCallback, useRef, useMemo } from 'react'; import { TableVirtuoso } from 'react-virtuoso'; import cloneDeep from 'lodash/cloneDeep'; import { IconTrash, IconAlertCircle, IconInfoCircle } from '@tabler/icons'; @@ -22,13 +22,13 @@ import Button from 'ui/Button'; const MIN_H = 35 * 2; -const TableRow = React.memo(({ children, item }) => {children}, (prevProps, nextProps) => { - const prevUid = prevProps?.item?.uid; - const nextUid = nextProps?.item?.uid; +const TableRow = React.memo(({ children, item }) => {children}, (prevProps, nextProps) => { + const prevUid = prevProps?.item?.variable?.uid; + const nextUid = nextProps?.item?.variable?.uid; return prevUid === nextUid && prevProps.children === nextProps.children; }); -const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentVariables, collection }) => { +const EnvironmentVariables = ({ environment, setIsModified, collection, searchQuery = '' }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const { globalEnvironments, activeGlobalEnvironmentUid, globalEnvironmentDraft } = useSelector( @@ -335,16 +335,38 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV }; }, []); + const filteredVariables = useMemo(() => { + const allVariables = formik.values.map((variable, index) => ({ variable, index })); + if (!searchQuery?.trim()) { + return allVariables; + } + + const query = searchQuery.toLowerCase().trim(); + + return allVariables.filter(({ variable, index }) => { + const isLastRow = index === formik.values.length - 1; + const isEmptyRow = !variable.name || variable.name.trim() === ''; + if (isLastRow && isEmptyRow) { + return true; + } + + const nameMatch = variable.name ? variable.name.toLowerCase().includes(query) : false; + const valueMatch = typeof variable.value === 'string' ? variable.value.toLowerCase().includes(query) : false; + + return !!(nameMatch || valueMatch); + }); + }, [formik.values, searchQuery]); + return ( variable.uid} + computeItemKey={(index, item) => item.variable.uid} fixedHeaderContent={() => ( @@ -354,8 +376,9 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV )} - itemContent={(index, variable) => { - const isLastRow = index === formik.values.length - 1; + + itemContent={(index, { variable, index: actualIndex }) => { + const isLastRow = actualIndex === formik.values.length - 1; const isEmptyRow = !variable.name || variable.name.trim() === ''; const isLastEmptyRow = isLastRow && isEmptyRow; @@ -366,7 +389,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV @@ -381,15 +404,15 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV autoCapitalize="off" spellCheck="false" className="mousetrap" - id={`${index}.name`} - name={`${index}.name`} + id={`${actualIndex}.name`} + name={`${actualIndex}.name`} value={variable.name} placeholder={isLastEmptyRow ? 'Name' : ''} - onChange={(e) => handleNameChange(index, e)} - onBlur={() => handleNameBlur(index)} - onKeyDown={(e) => handleNameKeyDown(index, e)} + onChange={(e) => handleNameChange(actualIndex, e)} + onBlur={() => handleNameBlur(actualIndex)} + onKeyDown={(e) => handleNameKeyDown(actualIndex, e)} /> - + @@ -397,12 +420,12 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV formik.setFieldValue(`${index}.value`, newValue, true)} + onChange={(newValue) => formik.setFieldValue(`${actualIndex}.value`, newValue, true)} onSave={handleSave} /> @@ -422,7 +445,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV diff --git a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/StyledWrapper.js b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/StyledWrapper.js index d6f9d20e3..697e489bd 100644 --- a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/StyledWrapper.js +++ b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/StyledWrapper.js @@ -94,8 +94,66 @@ const StyledWrapper = styled.div` .actions { display: flex; + align-items: center; gap: 2px; - + + .search-container { + position: relative; + top: 6px; + display: flex; + align-items: center; + // margin-right: 8px; + // margin-top: 8px; + + .search-icon { + position: absolute; + // left: 10px; + color: ${(props) => props.theme.colors.text.muted}; + pointer-events: none; + } + + .search-input { + width: 200px; + padding: 5px 32px 5px 32px; + border: 1px solid ${(props) => props.theme.input.border}; + border-radius: ${(props) => props.theme.border.radius.sm}; + background: ${(props) => props.theme.input.bg}; + color: ${(props) => props.theme.text}; + font-size: ${(props) => props.theme.font.size.base}; + outline: none; + transition: border-color 0.15s ease; + + &:focus { + border-color: ${(props) => props.theme.input.focusBorder}; + } + + &::placeholder { + color: ${(props) => props.theme.input.placeholder.color}; + opacity: ${(props) => props.theme.input.placeholder.opacity}; + } + } + + .clear-search { + position: absolute; + right: 8px; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; + color: ${(props) => props.theme.colors.text.muted}; + background: transparent; + border: none; + cursor: pointer; + border-radius: ${(props) => props.theme.border.radius.sm}; + transition: all 0.15s ease; + + &:hover { + color: ${(props) => props.theme.text}; + background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + } + } + } + button { display: inline-flex; align-items: center; diff --git a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/index.js b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/index.js index 362e33ac6..b197c7810 100644 --- a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/index.js +++ b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/index.js @@ -1,6 +1,7 @@ -import { IconCopy, IconEdit, IconTrash, IconCheck, IconX } from '@tabler/icons'; +import { IconCopy, IconEdit, IconTrash, IconCheck, IconX, IconSearch } from '@tabler/icons'; import { useState, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import useDebounce from 'hooks/useDebounce'; import { renameGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments'; import { validateName, validateNameError } from 'utils/common/regex'; import toast from 'react-hot-toast'; @@ -18,6 +19,8 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => { const [isRenaming, setIsRenaming] = useState(false); const [newName, setNewName] = useState(''); const [nameError, setNameError] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const debouncedSearchQuery = useDebounce(searchQuery, 300); const inputRef = useRef(null); const validateEnvironmentName = (name) => { @@ -164,6 +167,29 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => { {nameError && isRenaming &&
{nameError}
}
+
+ + setSearchQuery(e.target.value)} + className="search-input" + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + spellCheck="false" + /> + {searchQuery && ( + + )} +
@@ -177,7 +203,12 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
- +
);