diff --git a/packages/bruno-app/src/components/EnvironmentVariablesTable/index.js b/packages/bruno-app/src/components/EnvironmentVariablesTable/index.js index 60b5c697e..8ae4e801d 100644 --- a/packages/bruno-app/src/components/EnvironmentVariablesTable/index.js +++ b/packages/bruno-app/src/components/EnvironmentVariablesTable/index.js @@ -38,7 +38,8 @@ const EnvironmentVariablesTable = ({ onDraftChange, onDraftClear, setIsModified, - renderExtraValueContent + renderExtraValueContent, + searchQuery = '' }) => { const { storedTheme } = useTheme(); const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments); @@ -393,13 +394,35 @@ const EnvironmentVariablesTable = ({ }; }, []); + 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 ( ( @@ -418,9 +441,9 @@ const EnvironmentVariablesTable = ({ )} 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; @@ -431,7 +454,7 @@ const EnvironmentVariablesTable = ({ @@ -446,15 +469,15 @@ const EnvironmentVariablesTable = ({ 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)} /> - + @@ -462,12 +485,12 @@ const EnvironmentVariablesTable = ({ formik.setFieldValue(`${index}.value`, newValue, true)} + onChange={(newValue) => formik.setFieldValue(`${actualIndex}.value`, newValue, true)} onSave={handleSave} /> @@ -488,7 +511,7 @@ const EnvironmentVariablesTable = ({ 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 8edbb1b7d..266c62681 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 @@ -9,7 +9,7 @@ import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; import EnvironmentVariablesTable from 'components/EnvironmentVariablesTable'; import { sensitiveFields } from './constants'; -const EnvironmentVariables = ({ environment, setIsModified, collection }) => { +const EnvironmentVariables = ({ environment, setIsModified, collection, searchQuery = '' }) => { const dispatch = useDispatch(); const environmentsDraft = collection?.environmentsDraft; @@ -111,6 +111,7 @@ const EnvironmentVariables = ({ environment, setIsModified, collection }) => { onDraftClear={handleDraftClear} setIsModified={setIsModified} renderExtraValueContent={renderExtraValueContent} + searchQuery={searchQuery} /> ); }; 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..8c6e532ba 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-input-wrapper { + position: relative; + display: flex; + align-items: center; + + .search-icon { + position: absolute; + left: 8px; + 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: 1px; + 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 78cbc4ee7..0ba185dbb 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, updateEnvironmentColor } from 'providers/ReduxStore/slices/collections/actions'; import { validateName, validateNameError } from 'utils/common/regex'; import toast from 'react-hot-toast'; @@ -19,7 +20,11 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => { const [isRenaming, setIsRenaming] = useState(false); const [newName, setNewName] = useState(''); const [nameError, setNameError] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const [isSearchExpanded, setIsSearchExpanded] = useState(false); + const debouncedSearchQuery = useDebounce(searchQuery, 300); const inputRef = useRef(null); + const searchInputRef = useRef(null); const validateEnvironmentName = (name) => { if (!name || name.trim() === '') { @@ -112,6 +117,23 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => { } }; + const handleSearchIconClick = () => { + setIsSearchExpanded(true); + setTimeout(() => { + searchInputRef.current?.focus(); + }, 50); + }; + + const handleClearSearch = () => { + setSearchQuery(''); + }; + + const handleSearchBlur = () => { + if (searchQuery === '') { + setIsSearchExpanded(false); + } + }; + const handleColorChange = (color) => { dispatch(updateEnvironmentColor(environment.uid, color, collection.uid)) .then(() => { @@ -176,6 +198,38 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => { {nameError && isRenaming &&
{nameError}
}
+ {isSearchExpanded ? ( +
+ + setSearchQuery(e.target.value)} + onBlur={handleSearchBlur} + className="search-input" + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + spellCheck="false" + /> + {searchQuery && ( + + )} +
+ ) : ( + + )} @@ -189,7 +243,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 bf8dc8e2d..a123798f3 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,5 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useRef, useMemo } from 'react'; +import { TableVirtuoso } from 'react-virtuoso'; import cloneDeep from 'lodash/cloneDeep'; import { useDispatch, useSelector } from 'react-redux'; import { @@ -8,7 +9,7 @@ import { } from 'providers/ReduxStore/slices/global-environments'; import EnvironmentVariablesTable from 'components/EnvironmentVariablesTable'; -const EnvironmentVariables = ({ environment, setIsModified, collection }) => { +const EnvironmentVariables = ({ environment, setIsModified, collection, searchQuery = '' }) => { const dispatch = useDispatch(); const { globalEnvironmentDraft } = useSelector((state) => state.globalEnvironments); @@ -46,6 +47,7 @@ const EnvironmentVariables = ({ environment, setIsModified, collection }) => { onDraftChange={handleDraftChange} onDraftClear={handleDraftClear} setIsModified={setIsModified} + searchQuery={searchQuery} /> ); }; 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..928513d0d 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,63 @@ const StyledWrapper = styled.div` .actions { display: flex; + align-items: center; gap: 2px; - + + .search-input-wrapper { + position: relative; + display: flex; + align-items: center; + + .search-icon { + position: absolute; + left: 8px; + 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: 1px; + 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 9f7d2705c..6d9219f9e 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, updateGlobalEnvironmentColor } from 'providers/ReduxStore/slices/global-environments'; import { validateName, validateNameError } from 'utils/common/regex'; import toast from 'react-hot-toast'; @@ -19,7 +20,11 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => { const [isRenaming, setIsRenaming] = useState(false); const [newName, setNewName] = useState(''); const [nameError, setNameError] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const [isSearchExpanded, setIsSearchExpanded] = useState(false); + const debouncedSearchQuery = useDebounce(searchQuery, 300); const inputRef = useRef(null); + const searchInputRef = useRef(null); const validateEnvironmentName = (name) => { if (!name || name.trim() === '') { @@ -111,6 +116,23 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => { } }; + const handleSearchIconClick = () => { + setIsSearchExpanded(true); + setTimeout(() => { + searchInputRef.current?.focus(); + }, 50); + }; + + const handleClearSearch = () => { + setSearchQuery(''); + }; + + const handleSearchBlur = () => { + if (searchQuery === '') { + setIsSearchExpanded(false); + } + }; + const handleColorChange = (color) => { dispatch(updateGlobalEnvironmentColor(environment.uid, color)) .then(() => { @@ -178,6 +200,38 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => { {nameError && isRenaming &&
{nameError}
}
+ {isSearchExpanded ? ( +
+ + setSearchQuery(e.target.value)} + onBlur={handleSearchBlur} + className="search-input" + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + spellCheck="false" + /> + {searchQuery && ( + + )} +
+ ) : ( + + )} @@ -191,7 +245,12 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
- +
);