diff --git a/src/App.css b/src/App.css index 7453fb22..0d06d85b 100644 --- a/src/App.css +++ b/src/App.css @@ -5,7 +5,7 @@ } .sui-canvas { - height: calc(100% - 4rem - 5rem); + height: calc(100% - 5rem); width: 100%; } diff --git a/src/App.styles.tsx b/src/App.styles.tsx index e1e16dc2..a126fba4 100644 --- a/src/App.styles.tsx +++ b/src/App.styles.tsx @@ -2,7 +2,7 @@ import styled from 'styled-components'; export const MainContentContainer = styled.div` display: flex; - height: 100vh; + height: calc(100vh - 4rem); width: 100%; `; diff --git a/src/components/itemBrowser/ItemBrowser.styles.tsx b/src/components/itemBrowser/ItemBrowser.styles.tsx index f86297aa..a0401ef8 100644 --- a/src/components/itemBrowser/ItemBrowser.styles.tsx +++ b/src/components/itemBrowser/ItemBrowser.styles.tsx @@ -9,6 +9,9 @@ export const ResourcesContainer = styled.div` ${mobileMediaQuery} { grid-template-columns: minmax(7.5rem, 1fr) minmax(7.5rem, 1fr); gap: 1rem; + max-height: 500px; + overflow: scroll; + height: auto; } `; @@ -88,3 +91,19 @@ export const BreadCrumbsWrapper = styled.div` display: flex; margin-bottom: 1rem; `; + +export const SearchInputWrapper = styled.div<{ hasSearchQuery?: boolean; isMobile?: boolean }>` + width: ${(props) => (props.isMobile ? '100%' : '16.25rem')}; + ${(props) => + props.hasSearchQuery && + ` + margin-bottom: 1rem; + `}; +`; + +export const EmptySearchResultContainer = styled.div` + padding: 0 3.75rem 0 calc(3.75rem - 1.25rem); + color: ${Colors.SECONDARY_FONT}; + font-size: 0.875rem; + text-align: center; +`; diff --git a/src/components/itemBrowser/ItemBrowser.tsx b/src/components/itemBrowser/ItemBrowser.tsx index f2a1b7a0..5013eb58 100644 --- a/src/components/itemBrowser/ItemBrowser.tsx +++ b/src/components/itemBrowser/ItemBrowser.tsx @@ -1,7 +1,10 @@ import { + AvailableIcons, BreadCrumb, PreviewCard as ChiliPreview, Colors, + Icon, + Input, Panel, PreviewCardVariant, PreviewType, @@ -15,7 +18,13 @@ import { useVariablePanelContext } from '../../contexts/VariablePanelContext'; import { ContentType } from '../../contexts/VariablePanelContext.types'; import { AssetType } from '../../utils/ApiTypes'; import { getDataIdForSUI, getDataTestIdForSUI } from '../../utils/dataIds'; -import { BreadCrumbsWrapper, LoadPageContainer, ResourcesContainer } from './ItemBrowser.styles'; +import { + BreadCrumbsWrapper, + LoadPageContainer, + ResourcesContainer, + SearchInputWrapper, + EmptySearchResultContainer, +} from './ItemBrowser.styles'; import { ItemCache } from './ItemCache'; const TOP_BAR_HEIGHT_REM = '4rem'; @@ -68,6 +77,8 @@ function ItemBrowser< }); const [isLoading, setIsLoading] = useState(false); const [list, setList] = useState[]>([]); + const [searchKeyWord, setSearchKeyWord] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); const moreData = !!nextPageToken?.token; const { @@ -94,7 +105,7 @@ function ItemBrowser< return { token: null, requested: true }; }); setList(() => []); - }, [navigationStack]); + }, [navigationStack, searchQuery]); useEffect(() => { setBreadcrumbStack([]); @@ -110,7 +121,6 @@ function ItemBrowser< // in case the useEffect runs again (dependencies change) while the promise did not resolve, the cleanup function // makes sure that the result that is not relevant anymore, won't affect the state. let ignore = false; - if (!nextPageToken.requested) return; setIsLoading(true); // declare the async data fetching function @@ -123,6 +133,7 @@ function ItemBrowser< collection: `/${navigationStack?.join('/') ?? ''}`, pageToken: nextPageToken.token ? nextPageToken.token : '', pageSize: 15, + filter: [searchQuery], }, {}, ); @@ -168,7 +179,7 @@ function ItemBrowser< ignore = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nextPageToken, contentType]); + }, [nextPageToken, contentType, searchQuery]); useEffect(() => { return () => { @@ -207,6 +218,8 @@ function ItemBrowser< } else { selectItem(listItem.instance); setNavigationStack([]); + setSearchQuery(''); + setSearchKeyWord(''); } }; @@ -251,6 +264,11 @@ function ItemBrowser< // eslint-disable-next-line no-nested-ternary const panelTitle = isMobileSize ? null : contentType === ContentType.IMAGE_PANEL ? imagePanelTitle : null; + const handleSearch = (keyword: string) => { + setSearchQuery(keyword); + setNavigationStack([]); + setBreadcrumbStack([]); + }; return ( - - { - const newNavigationStack = navigationStack?.splice(0, navigationStack.indexOf(breadCrumb) + 1); - const newBreadcrumbStack = breadcrumbStack?.splice(0, breadcrumbStack.indexOf(breadCrumb) + 1); - setNavigationStack(newNavigationStack); - setBreadcrumbStack(newBreadcrumbStack); - }} - /> - + {connectorCapabilities[connectorId]?.filtering && ( + + setSearchKeyWord(e.target.value)} + onBlur={() => handleSearch(searchKeyWord)} + width="260px" + leftIcon={{ + icon: ( + + ), + label: 'Search icon', + }} + dataId={getDataIdForSUI('media-panel-search-input')} + dataTestId={getDataTestIdForSUI('media-panel-search-input')} + rightIcon={ + searchKeyWord + ? { + label: 'Clear search icon', + icon: ( + + ), + onClick: () => { + setSearchKeyWord(''); + setSearchQuery(''); + }, + } + : undefined + } + isHighlightOnClick + /> + + )} + {!searchQuery && ( + + { + const newNavigationStack = navigationStack?.splice( + 0, + navigationStack.indexOf(breadCrumb) + 1, + ); + const newBreadcrumbStack = breadcrumbStack?.splice( + 0, + breadcrumbStack.indexOf(breadCrumb) + 1, + ); + setNavigationStack(newNavigationStack); + setBreadcrumbStack(newBreadcrumbStack); + }} + /> + + )} + {elements.length === 0 && !isLoading && searchQuery && ( + + No search results found. Maybe try another keyword? + + )} diff --git a/src/tests/LeftPanel.test.tsx b/src/tests/LeftPanel.test.tsx index ad709227..234cdac3 100644 --- a/src/tests/LeftPanel.test.tsx +++ b/src/tests/LeftPanel.test.tsx @@ -1,6 +1,6 @@ import { getDataTestId } from '@chili-publish/grafx-shared-components'; import EditorSDK from '@chili-publish/studio-sdk'; -import { render, waitFor } from '@testing-library/react'; +import { render, waitFor, screen } from '@testing-library/react'; import { mock } from 'jest-mock-extended'; import { act } from 'react-dom/test-utils'; import LeftPanel from '../components/layout-panels/leftPanel/LeftPanel'; @@ -10,11 +10,11 @@ import { mockConnectors } from './mocks/mockConnectors'; import { variables } from './mocks/mockVariables'; import { getDataTestIdForSUI } from '../utils/dataIds'; +jest.mock('@chili-publish/studio-sdk'); +const mockSDK = mock(); + beforeEach(() => { - jest.mock('@chili-publish/studio-sdk'); - const mockSDK = mock(); global.URL.createObjectURL = jest.fn(); - mockSDK.mediaConnector.query = jest .fn() .mockImplementation() @@ -207,4 +207,58 @@ describe('Image Panel', () => { expect(window.SDK.variable.setImageVariableConnector).toBeCalledTimes(1); expect(window.SDK.variable.setValue).toBeCalledTimes(1); }); + test('Do not render search input when filtering is not supported', async () => { + mockSDK.mediaConnector.getCapabilities = jest + .fn() + .mockImplementation() + .mockReturnValue( + Promise.resolve({ + parsedData: { + copy: false, + detail: true, + filtering: false, + query: true, + remove: false, + upload: false, + }, + }), + ); + + const { getAllByTestId, getAllByRole } = render( + + + , + ); + const imagePicker = await waitFor(() => getAllByTestId(getDataTestId('image-picker-content'))[0]); + await act(async () => { + imagePicker.click(); + }); + const image = getAllByRole('img', { name: /grafx/i })[0]; + + await act(async () => { + image.click(); + }); + + const input = screen.queryByTestId(getDataTestIdForSUI('media-panel-search-input')); + expect(input).toBeNull(); + }); + test('Render search input when filtering is supported', async () => { + const { getAllByTestId, getAllByRole, getByTestId } = render( + + + , + ); + const imagePicker = await waitFor(() => getAllByTestId(getDataTestId('image-picker-content'))[0]); + await act(async () => { + imagePicker.click(); + }); + const image = getAllByRole('img', { name: /grafx/i })[0]; + + await act(async () => { + image.click(); + }); + + const input = getByTestId(getDataTestIdForSUI('media-panel-search-input')); + expect(input).toBeInTheDocument(); + }); });