diff --git a/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestsTabs.tsx b/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestsTabs.tsx
index b5b19757b297..0f707a401e57 100644
--- a/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestsTabs.tsx
+++ b/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestsTabs.tsx
@@ -307,6 +307,7 @@ export const ChangeRequestsTabs = ({
onChange={setSearchValue}
hasFilters
getSearchContext={getSearchContext}
+ id="changeRequestList"
/>
}
/>
diff --git a/frontend/src/component/common/Search/Search.test.tsx b/frontend/src/component/common/Search/Search.test.tsx
new file mode 100644
index 000000000000..200d4828e049
--- /dev/null
+++ b/frontend/src/component/common/Search/Search.test.tsx
@@ -0,0 +1,59 @@
+import { createLocalStorage } from 'utils/createLocalStorage';
+import { render } from 'utils/testRenderer';
+import { fireEvent, screen } from '@testing-library/react';
+import { UIProviderContainer } from '../../providers/UIProvider/UIProviderContainer';
+import { Search } from './Search';
+import { SEARCH_INPUT } from 'utils/testIds';
+
+const testDisplayComponent = (
+
+ {}}
+ id="localStorageId"
+ getSearchContext={() => ({
+ data: [],
+ columns: [],
+ searchValue: '',
+ })}
+ />
+
+);
+
+test('should read saved query from local storage', async () => {
+ const { value, setValue } = createLocalStorage(
+ 'Search:localStorageId:v1',
+ {}
+ );
+ setValue({
+ query: 'oldquery',
+ });
+
+ render(testDisplayComponent);
+
+ const input = screen.getByTestId(SEARCH_INPUT);
+
+ input.focus();
+
+ await screen.findByText('oldquery'); // local storage saved search query
+
+ screen.getByText('oldquery').click(); // click history hint
+
+ expect(screen.getByDisplayValue('oldquery')).toBeInTheDocument(); // check if input updates
+
+ fireEvent.change(input, { target: { value: 'newquery' } });
+
+ expect(screen.getByText('newquery')).toBeInTheDocument(); // new saved query updated
+});
+
+test('should update saved query without local storage', async () => {
+ render(testDisplayComponent);
+
+ const input = screen.getByTestId(SEARCH_INPUT);
+
+ input.focus();
+
+ fireEvent.change(input, { target: { value: 'newquery' } });
+
+ expect(screen.getByText('newquery')).toBeInTheDocument(); // new saved query updated
+});
diff --git a/frontend/src/component/common/Search/Search.tsx b/frontend/src/component/common/Search/Search.tsx
index 85b5a3b380db..31f5cfb9338d 100644
--- a/frontend/src/component/common/Search/Search.tsx
+++ b/frontend/src/component/common/Search/Search.tsx
@@ -1,15 +1,17 @@
import React, { useRef, useState } from 'react';
import { useAsyncDebounce } from 'react-table';
import { Box, IconButton, InputBase, styled, Tooltip } from '@mui/material';
-import { Search as SearchIcon, Close } from '@mui/icons-material';
+import { Close, Search as SearchIcon } from '@mui/icons-material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { SearchSuggestions } from './SearchSuggestions/SearchSuggestions';
import { IGetSearchContextOutput } from 'hooks/useSearch';
import { useKeyboardShortcut } from 'hooks/useKeyboardShortcut';
import { SEARCH_INPUT } from 'utils/testIds';
import { useOnClickOutside } from 'hooks/useOnClickOutside';
+import { useSavedQuery } from './useSavedQuery';
interface ISearchProps {
+ id?: string;
initialValue?: string;
onChange: (value: string) => void;
onFocus?: () => void;
@@ -66,6 +68,7 @@ const StyledClose = styled(Close)(({ theme }) => ({
export const Search = ({
initialValue = '',
+ id,
onChange,
onFocus,
onBlur,
@@ -86,12 +89,15 @@ export const Search = ({
onBlur?.();
};
+ const { savedQuery, setSavedQuery } = useSavedQuery(id);
+
const [value, setValue] = useState(initialValue);
const debouncedOnChange = useAsyncDebounce(onChange, debounceTime);
const onSearchChange = (value: string) => {
debouncedOnChange(value);
setValue(value);
+ setSavedQuery(value);
};
const hotkey = useKeyboardShortcut(
@@ -163,6 +169,7 @@ export const Search = ({
/>
+
}
diff --git a/frontend/src/component/common/Search/SearchSuggestions/SearchInstructions/SearchInstructions.tsx b/frontend/src/component/common/Search/SearchSuggestions/SearchInstructions/SearchInstructions.tsx
index 651320843942..bfdf6cf6fe1d 100644
--- a/frontend/src/component/common/Search/SearchSuggestions/SearchInstructions/SearchInstructions.tsx
+++ b/frontend/src/component/common/Search/SearchSuggestions/SearchInstructions/SearchInstructions.tsx
@@ -7,12 +7,16 @@ const StyledHeader = styled('span')(({ theme }) => ({
color: theme.palette.text.primary,
}));
-const StyledCode = styled('span')(({ theme }) => ({
+export const StyledCode = styled('span')(({ theme }) => ({
backgroundColor: theme.palette.background.elevation2,
color: theme.palette.text.primary,
padding: theme.spacing(0.2, 1),
borderRadius: theme.spacing(0.5),
cursor: 'pointer',
+ '&:hover': {
+ transition: 'background-color 0.2s ease-in-out',
+ backgroundColor: theme.palette.seen.primary,
+ },
}));
const StyledFilterHint = styled('p')(({ theme }) => ({
@@ -49,11 +53,18 @@ export const SearchInstructions: VFC = ({
{filters.map(filter => (
{filter.header}:{' '}
- onClick(firstFilterOption(filter))}
- >
- {firstFilterOption(filter)}
-
+ 0}
+ show={
+
+ onClick(firstFilterOption(filter))
+ }
+ >
+ {firstFilterOption(filter)}
+
+ }
+ />
1}
show={
diff --git a/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.tsx b/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.tsx
index 653887c7846c..5c10f57ac4c9 100644
--- a/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.tsx
+++ b/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.tsx
@@ -1,4 +1,4 @@
-import { FilterList } from '@mui/icons-material';
+import { FilterList, History } from '@mui/icons-material';
import { Box, Divider, Paper, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import {
@@ -7,9 +7,12 @@ import {
getFilterValues,
IGetSearchContextOutput,
} from 'hooks/useSearch';
-import { useMemo, VFC } from 'react';
+import { VFC } from 'react';
import { SearchDescription } from './SearchDescription/SearchDescription';
-import { SearchInstructions } from './SearchInstructions/SearchInstructions';
+import {
+ SearchInstructions,
+ StyledCode,
+} from './SearchInstructions/SearchInstructions';
const StyledPaper = styled(Paper)(({ theme }) => ({
position: 'absolute',
@@ -31,6 +34,10 @@ const StyledBox = styled(Box)(({ theme }) => ({
gap: theme.spacing(2),
}));
+const StyledHistory = styled(History)(({ theme }) => ({
+ color: theme.palette.text.secondary,
+}));
+
const StyledFilterList = styled(FilterList)(({ theme }) => ({
color: theme.palette.text.secondary,
}));
@@ -40,17 +47,10 @@ const StyledDivider = styled(Divider)(({ theme }) => ({
margin: theme.spacing(1.5, 0),
}));
-const StyledCode = styled('span')(({ theme }) => ({
- backgroundColor: theme.palette.background.elevation2,
- color: theme.palette.text.primary,
- padding: theme.spacing(0.2, 0.5),
- borderRadius: theme.spacing(0.5),
- cursor: 'pointer',
-}));
-
interface SearchSuggestionsProps {
getSearchContext: () => IGetSearchContextOutput;
onSuggestion: (suggestion: string) => void;
+ savedQuery?: string;
}
const quote = (item: string) => (item.includes(' ') ? `"${item}"` : item);
@@ -60,6 +60,7 @@ const randomIndex = (arr: any[]) => Math.floor(Math.random() * arr.length);
export const SearchSuggestions: VFC = ({
getSearchContext,
onSuggestion,
+ savedQuery,
}) => {
const searchContext = getSearchContext();
@@ -108,6 +109,23 @@ export const SearchSuggestions: VFC = ({
return (
+
+
+
+ onSuggestion(savedQuery || '')}
+ >
+ {savedQuery}
+
+
+
+ >
+ }
+ />
+
diff --git a/frontend/src/component/common/Search/useSavedQuery.ts b/frontend/src/component/common/Search/useSavedQuery.ts
new file mode 100644
index 000000000000..841b9ee97fff
--- /dev/null
+++ b/frontend/src/component/common/Search/useSavedQuery.ts
@@ -0,0 +1,28 @@
+import { createLocalStorage } from 'utils/createLocalStorage';
+import { useEffect, useState } from 'react';
+
+// if you provided persistent id the query will be persisted in local storage
+export const useSavedQuery = (id?: string) => {
+ const { value, setValue } = createLocalStorage(
+ `Search:${id || 'default'}:v1`,
+ {
+ query: '',
+ }
+ );
+ const [savedQuery, setSavedQuery] = useState(value.query);
+
+ useEffect(() => {
+ if (id && savedQuery.trim().length > 0) {
+ setValue({ query: savedQuery });
+ }
+ }, [id, savedQuery]);
+
+ return {
+ savedQuery,
+ setSavedQuery: (newValue: string) => {
+ if (newValue.trim().length > 0) {
+ setSavedQuery(newValue);
+ }
+ },
+ };
+};
diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx
index 91a5c0ca0ee3..e9d8e5992c05 100644
--- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx
+++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx
@@ -554,6 +554,7 @@ export const ProjectFeatureToggles = ({
onBlur={() => setShowTitle(true)}
hasFilters
getSearchContext={getSearchContext}
+ id="projectFeatureToggles"
/>
}
/>
@@ -612,6 +613,7 @@ export const ProjectFeatureToggles = ({
onChange={setSearchValue}
hasFilters
getSearchContext={getSearchContext}
+ id="projectFeatureToggles"
/>
}
/>