Launch automatically the search in the asset store when typing (#4725)

This commit is contained in:
Florian Rival
2022-12-21 22:12:05 +01:00
committed by GitHub
parent b8f04515f3
commit 893257491a
5 changed files with 249 additions and 191 deletions

View File

@@ -115,8 +115,7 @@ export const AssetStoreContext = React.createContext<AssetStoreState>({
backToPreviousPage: () => {},
openHome: () => {},
clearHistory: () => {},
openSearchIfNeeded: () => {},
activateTextualSearch: () => {},
openSearchResultPage: () => {},
openTagPage: tag => {},
openPackPage: assetPack => {},
openDetailPage: assetShortHeader => {},
@@ -392,7 +391,7 @@ export const AssetStoreStateProvider = ({
const searchResults: ?Array<AssetShortHeader> = useSearchItem(
assetShortHeadersById,
getAssetShortHeaderSearchTerms,
currentPage.ignoreTextualSearch ? '' : searchText,
searchText,
chosenCategory,
chosenFilters,
searchFilters

View File

@@ -9,12 +9,10 @@ import {
import { type PrivateAssetPackListingData } from '../Utils/GDevelopServices/Shop';
export type AssetStorePageState = {|
isOnHomePage: boolean,
openedAssetPack: PublicAssetPack | PrivateAssetPack | null,
openedAssetShortHeader: ?AssetShortHeader,
openedPrivateAssetPackListingData: ?PrivateAssetPackListingData,
filtersState: FiltersState,
ignoreTextualSearch: boolean,
scrollPosition?: ?number,
|};
@@ -23,8 +21,7 @@ export type NavigationState = {|
backToPreviousPage: () => void,
openHome: () => void,
clearHistory: () => void,
openSearchIfNeeded: () => void,
activateTextualSearch: () => void,
openSearchResultPage: () => void,
openTagPage: string => void,
openPackPage: (PublicAssetPack | PrivateAssetPack) => void,
openPrivateAssetPackInformationPage: PrivateAssetPackListingData => void,
@@ -40,21 +37,29 @@ const noFilter: FiltersState = {
};
export const assetStoreHomePageState: AssetStorePageState = {
isOnHomePage: true,
openedAssetShortHeader: null,
openedAssetPack: null,
openedPrivateAssetPackListingData: null,
filtersState: noFilter,
ignoreTextualSearch: false,
};
const searchPageState: AssetStorePageState = {
isOnHomePage: false,
openedAssetShortHeader: null,
openedAssetPack: null,
openedPrivateAssetPackListingData: null,
filtersState: noFilter,
ignoreTextualSearch: false,
};
export const isHomePage = (pageState: AssetStorePageState) => {
return pageState === assetStoreHomePageState;
};
export const isSearchResultPage = (pageState: AssetStorePageState) => {
return (
!isHomePage(pageState) &&
!pageState.openedAssetShortHeader &&
!pageState.openedPrivateAssetPackListingData
);
};
type AssetStorePageHistory = {|
@@ -82,6 +87,8 @@ export const useNavigation = (): NavigationState => {
},
clearHistory: () => {
setHistory(previousHistory => {
if (previousHistory.previousPages.length <= 1) return previousHistory;
const currentPage =
previousHistory.previousPages[
previousHistory.previousPages.length - 1
@@ -89,28 +96,20 @@ export const useNavigation = (): NavigationState => {
return { previousPages: [assetStoreHomePageState, currentPage] };
});
},
openSearchIfNeeded: () => {
const currentPage = previousPages[previousPages.length - 1];
if (currentPage.isOnHomePage || currentPage.openedAssetShortHeader) {
setHistory({ previousPages: [...previousPages, searchPageState] });
}
},
activateTextualSearch: () => {
openSearchResultPage: () => {
setHistory(previousHistory => {
const currentPage =
previousHistory.previousPages[
previousHistory.previousPages.length - 1
];
if (currentPage.isOnHomePage || currentPage.openedAssetShortHeader) {
if (!isSearchResultPage(currentPage)) {
return {
...previousHistory,
previousPages: [
...previousHistory.previousPages,
searchPageState,
],
};
} else if (currentPage.ignoreTextualSearch) {
currentPage.ignoreTextualSearch = false;
return { previousPages: [...previousHistory.previousPages] };
}
return previousHistory;
@@ -122,7 +121,6 @@ export const useNavigation = (): NavigationState => {
previousPages: [
...previousHistory.previousPages,
{
isOnHomePage: false,
openedAssetShortHeader: null,
openedAssetPack: null,
openedPrivateAssetPackListingData: null,
@@ -136,7 +134,6 @@ export const useNavigation = (): NavigationState => {
removeFilter: () => {},
setChosenCategory: () => {},
},
ignoreTextualSearch: true,
},
],
}));
@@ -147,7 +144,6 @@ export const useNavigation = (): NavigationState => {
previousPages: [
...previousHistory.previousPages,
{
isOnHomePage: false,
openedAssetShortHeader: null,
openedAssetPack: assetPack,
openedPrivateAssetPackListingData: null,
@@ -165,7 +161,6 @@ export const useNavigation = (): NavigationState => {
removeFilter: () => {},
setChosenCategory: () => {},
},
ignoreTextualSearch: true,
},
],
}));
@@ -178,12 +173,10 @@ export const useNavigation = (): NavigationState => {
previousPages: [
...previousHistory.previousPages,
{
isOnHomePage: false,
openedAssetShortHeader: null,
openedAssetPack: null,
openedPrivateAssetPackListingData: assetPack,
filtersState: noFilter,
ignoreTextualSearch: true,
},
],
}));
@@ -194,12 +187,10 @@ export const useNavigation = (): NavigationState => {
previousPages: [
...previousHistory.previousPages,
{
isOnHomePage: false,
openedAssetShortHeader: assetShortHeader,
openedAssetPack: null,
openedPrivateAssetPackListingData: null,
filtersState: noFilter,
ignoreTextualSearch: true,
},
],
}));

View File

@@ -48,6 +48,7 @@ import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
import PrivateAssetPackPurchaseDialog from './PrivateAssets/PrivateAssetPackPurchaseDialog';
import { LineStackLayout } from '../UI/Layout';
import Paper from '../UI/Paper';
import { isHomePage, isSearchResultPage } from './AssetStoreNavigator';
type Props = {||};
@@ -95,12 +96,15 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
assetPackRandomOrdering,
} = React.useContext(AssetStoreContext);
const {
isOnHomePage,
openedAssetPack,
openedAssetShortHeader,
openedPrivateAssetPackListingData,
filtersState,
} = navigationState.getCurrentPage();
const isOnHomePage = isHomePage(navigationState.getCurrentPage());
const isOnSearchResultPage = isSearchResultPage(
navigationState.getCurrentPage()
);
const searchBar = React.useRef<?SearchBarInterface>(null);
const shouldAutofocusSearchbar = useShouldAutofocusSearchbar();
@@ -224,11 +228,12 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
Window.openExternalURL(assetPack.externalWebLink);
} else {
saveScrollPosition();
setSearchText('');
navigationState.openPackPage(assetPack);
setIsFiltersPanelOpen(true);
}
},
[navigationState, saveScrollPosition]
[navigationState, saveScrollPosition, setSearchText]
);
// When a private pack is selected from the home page,
@@ -248,6 +253,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
assetPackKind: 'private',
});
setSearchText('');
navigationState.openPrivateAssetPackInformationPage(
assetPackListingData
);
@@ -255,6 +261,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
}
// The user has received the pack, open it.
setSearchText('');
sendAssetPackOpened({
assetPackName: assetPackListingData.name,
assetPackId: assetPackListingData.id,
@@ -265,7 +272,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
navigationState.openPackPage(receivedAssetPack);
setIsFiltersPanelOpen(true);
},
[receivedAssetPacks, saveScrollPosition, navigationState]
[receivedAssetPacks, saveScrollPosition, navigationState, setSearchText]
);
// If the user has received the pack they are currently viewing,
@@ -286,6 +293,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
);
if (receivedAssetPack) {
// The user has received the pack, close the pack information dialog, and open the pack in the search.
setSearchText('');
setIsFiltersPanelOpen(true);
saveScrollPosition();
navigationState.openPackPage(receivedAssetPack);
@@ -297,6 +305,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
purchasingPrivateAssetPackListingData,
navigationState,
saveScrollPosition,
setSearchText,
]
);
@@ -312,6 +321,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
publicAssetPacks &&
publicAssetPacks.starterPacks.find(pack => pack.tag === tag);
saveScrollPosition();
setSearchText('');
if (privateAssetPack) {
navigationState.openPackPage(privateAssetPack);
} else if (publicAssetPack) {
@@ -323,6 +333,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
setIsFiltersPanelOpen(true);
},
[
setSearchText,
receivedAssetPacks,
publicAssetPacks,
saveScrollPosition,
@@ -344,6 +355,18 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
[shouldAutofocusSearchbar]
);
React.useLayoutEffect(
() => {
if (isOnHomePage) {
clearAllFilters(assetFiltersState);
setIsFiltersPanelOpen(false);
}
},
// assetFiltersState is not stable, so don't list it.
// eslint-disable-next-line react-hooks/exhaustive-deps
[isOnHomePage]
);
return (
<>
<ResponsiveWindowMeasurer>
@@ -361,6 +384,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
key="back-discover"
tooltip={t`Back to discover`}
onClick={() => {
setSearchText('');
navigationState.openHome();
setScrollUpdateIsNeeded();
clearAllFilters(assetFiltersState);
@@ -374,13 +398,24 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
<SearchBar
placeholder={t`Search assets`}
value={searchText}
onChange={setSearchText}
onRequestSearch={() => {
// Clear the history
navigationState.activateTextualSearch();
navigationState.clearHistory();
setIsFiltersPanelOpen(true);
}}
onChange={
isOnSearchResultPage
? // An existing search is already being done: just update the
// search text and the asset store will update the search results.
setSearchText
: (newValue: string) => {
setSearchText(newValue);
// A new search is being initiated: navigate to the search page,
// and clear the history as a new search was launched.
if (!!newValue) {
navigationState.clearHistory();
navigationState.openSearchResultPage();
setIsFiltersPanelOpen(true);
}
}
}
onRequestSearch={() => {}}
ref={searchBar}
id="asset-store-search-bar"
/>
@@ -407,12 +442,6 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
onClick={() => {
navigationState.backToPreviousPage();
setScrollUpdateIsNeeded();
if (
navigationState.getCurrentPage().isOnHomePage
) {
clearAllFilters(assetFiltersState);
setIsFiltersPanelOpen(false);
}
}}
/>
</Column>
@@ -497,7 +526,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
<AssetStoreFilterPanel
assetFiltersState={assetFiltersState}
onChoiceChange={() => {
navigationState.openSearchIfNeeded();
navigationState.openSearchResultPage();
}}
/>
</Line>
@@ -507,13 +536,19 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
</ScrollView>
</Column>
)}
{isOnHomePage &&
!(publicAssetPacks && privateAssetPacks) &&
!error && <PlaceholderLoader />}
{isOnHomePage &&
publicAssetPacks &&
privateAssetPacks &&
assetPackRandomOrdering && (
{isOnHomePage ? (
error ? (
<PlaceholderError onRetry={fetchAssetsAndFilters}>
<AlertMessage kind="error">
<Trans>
An error occurred when fetching the asset store
content. Please try again later.
</Trans>
</AlertMessage>
</PlaceholderError>
) : publicAssetPacks &&
privateAssetPacks &&
assetPackRandomOrdering ? (
<AssetsHome
ref={assetsHome}
publicAssetPacks={publicAssetPacks}
@@ -522,43 +557,30 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
onPublicAssetPackSelection={selectPublicAssetPack}
onPrivateAssetPackSelection={selectPrivateAssetPack}
/>
)}
{!isOnHomePage &&
!openedAssetShortHeader &&
!openedPrivateAssetPackListingData && (
<BoxSearchResults
ref={boxSearchResults}
baseSize={128}
onRetry={fetchAssetsAndFilters}
error={error}
searchItems={searchResults}
renderSearchItem={(assetShortHeader, size) => (
<AssetCard
size={size}
onOpenDetails={() =>
onOpenDetails(assetShortHeader)
}
assetShortHeader={assetShortHeader}
/>
)}
noResultPlaceholder={
<NoResultPlaceholder
onClear={() => clearAllFilters(assetFiltersState)}
/>
}
/>
)}
{isOnHomePage && error && (
<PlaceholderError onRetry={fetchAssetsAndFilters}>
<AlertMessage kind="error">
<Trans>
An error occurred when fetching the asset store
content. Please try again later.
</Trans>
</AlertMessage>
</PlaceholderError>
)}
{openedAssetShortHeader && (
) : (
<PlaceholderLoader />
)
) : isOnSearchResultPage ? (
<BoxSearchResults
ref={boxSearchResults}
baseSize={128}
onRetry={fetchAssetsAndFilters}
error={error}
searchItems={searchResults}
renderSearchItem={(assetShortHeader, size) => (
<AssetCard
size={size}
onOpenDetails={() => onOpenDetails(assetShortHeader)}
assetShortHeader={assetShortHeader}
/>
)}
noResultPlaceholder={
<NoResultPlaceholder
onClear={() => clearAllFilters(assetFiltersState)}
/>
}
/>
) : openedAssetShortHeader ? (
<AssetDetails
ref={assetDetails}
onTagSelection={selectTag}
@@ -566,8 +588,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
onOpenDetails={onOpenDetails}
onAssetLoaded={applyBackScrollPosition}
/>
)}
{!!openedPrivateAssetPackListingData && (
) : !!openedPrivateAssetPackListingData ? (
<PrivateAssetPackInformationPage
privateAssetPackListingData={
openedPrivateAssetPackListingData
@@ -581,7 +602,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
!!purchasingPrivateAssetPackListingData
}
/>
)}
) : null}
{!!purchasingPrivateAssetPackListingData && (
<PrivateAssetPackPurchaseDialog
privateAssetPackListingData={

View File

@@ -23,6 +23,7 @@ import Search from './CustomSvgIcons/Search';
import Cross from './CustomSvgIcons/Cross';
import GDevelopThemeContext from './Theme/ThemeContext';
import { type GDevelopTheme } from './Theme';
import { useDebounce } from '../Utils/UseDebounce';
type TagsHandler = {|
remove: string => void,
@@ -161,6 +162,8 @@ export type SearchBarInterface = {|
blur: () => void,
|};
const noop = () => {};
/**
* Material design search bar,
* inspired from https://github.com/TeamWertarbyte/material-ui-search-bar
@@ -230,10 +233,20 @@ const SearchBar = React.forwardRef<Props, SearchBarInterface>(
focused: isInputFocused,
});
const changeValue = React.useCallback(
newValue => {
setValue(newValue || '');
onChange && onChange(newValue || '');
const debouncedOnChange = useDebounce(onChange ? onChange : noop, 250);
const changeValueDebounced = React.useCallback(
(newValue: string) => {
setValue(newValue);
debouncedOnChange(newValue);
},
[debouncedOnChange, setValue]
);
const changeValueImmediately = React.useCallback(
(newValue: string) => {
setValue(newValue);
onChange && onChange(newValue);
},
[onChange, setValue]
);
@@ -270,7 +283,7 @@ const SearchBar = React.forwardRef<Props, SearchBarInterface>(
const handleBlur = () => {
setIsInputFocused(false);
if (!value || value.trim() === '') {
changeValue('');
changeValueImmediately('');
}
};
@@ -279,11 +292,11 @@ const SearchBar = React.forwardRef<Props, SearchBarInterface>(
};
const handleInput = (e: {| target: {| value: string |} |}) => {
changeValue(e.target.value);
changeValueDebounced(e.target.value);
};
const handleCancel = () => {
changeValue('');
changeValueImmediately('');
focus();
};
@@ -297,7 +310,7 @@ const SearchBar = React.forwardRef<Props, SearchBarInterface>(
const handleAutocompleteInput = (
event: any,
newValue: string,
newValue: ?string,
reason:
| 'create-option'
| 'select-option'
@@ -307,26 +320,35 @@ const SearchBar = React.forwardRef<Props, SearchBarInterface>(
) => {
// Called when the value of the autocomplete changes.
if (reason === 'select-option') {
tagsHandler && tagsHandler.add(newValue);
tagsHandler && tagsHandler.add(newValue || '');
// Clear the value that was entered as an option was selected.
changeValueImmediately('');
// Clear this value to make sure the autocomplete doesn't keep the
// last typed value in memory.
setAutocompleteValue('');
} else {
changeValue(newValue);
changeValueImmediately(newValue || '');
}
};
const handleAutocompleteInputChange = (
event: any,
newValue: string,
newValue: ?string,
reason: 'reset' | 'input' | 'clear'
) => {
// Called when the value of the input within the autocomplete changes.
if (reason === 'reset') {
// Happens when user selects an option
setValue('');
// Happens when user selects an option. Do as for 'select-option':
// Clear the value that was entered as an option was selected.
changeValueImmediately('');
// Clear this value to make sure the autocomplete doesn't keep the
// last typed value in memory.
setAutocompleteValue('');
} else {
setValue(newValue);
changeValueDebounced(newValue || '');
}
};

View File

@@ -7,33 +7,40 @@ import paperDecorator from '../PaperDecorator';
import SearchBar from '../../UI/SearchBar';
import { useFilters } from '../../UI/Search/FiltersChooser';
import { ColumnStackLayout } from '../../UI/Layout';
import Text from '../../UI/Text';
export default {
title: 'UI Building Blocks/SearchBar',
component: SearchBar,
decorators: [muiDecorator, paperDecorator],
decorators: [paperDecorator, muiDecorator],
};
const Vanilla = () => {
const [value, setValue] = React.useState<string>('');
return (
<SearchBar
value={value}
onChange={setValue}
onRequestSearch={action('request search')}
/>
<>
<Text>Value in state is: "{value}".</Text>
<SearchBar
value={value}
onChange={setValue}
onRequestSearch={action('request search')}
/>
</>
);
};
const WithPlaceholder = () => {
const [value, setValue] = React.useState<string>('');
return (
<SearchBar
value={value}
onChange={setValue}
onRequestSearch={action('request search')}
placeholder="Search with placeholder"
/>
<>
<Text>Value in state is: "{value}".</Text>
<SearchBar
value={value}
onChange={setValue}
onRequestSearch={action('request search')}
placeholder="Search with placeholder"
/>
</>
);
};
@@ -42,70 +49,82 @@ const Disabled = () => {
'something typed in disabled field'
);
return (
<SearchBar
value={value}
onChange={setValue}
onRequestSearch={action('request search')}
disabled
/>
<>
<Text>Value in state is: "{value}".</Text>
<SearchBar
value={value}
onChange={setValue}
onRequestSearch={action('request search')}
disabled
/>
</>
);
};
const Integrated = () => {
const [value, setValue] = React.useState<string>('');
return (
<SearchBar
value={value}
onChange={setValue}
onRequestSearch={action('request search')}
aspect="integrated-search-bar"
placeholder="Search integrated"
/>
<>
<Text>Value in state is: "{value}".</Text>
<SearchBar
value={value}
onChange={setValue}
onRequestSearch={action('request search')}
aspect="integrated-search-bar"
placeholder="Search integrated"
/>
</>
);
};
const WithHelpIcon = () => {
const [value, setValue] = React.useState<string>('');
return (
<SearchBar
value={value}
onChange={setValue}
onRequestSearch={action('request search')}
helpPagePath="https://gdevelop.io"
placeholder="Search with help icon"
/>
<>
<Text>Value in state is: "{value}".</Text>
<SearchBar
value={value}
onChange={setValue}
onRequestSearch={action('request search')}
helpPagePath="https://gdevelop.io"
placeholder="Search with help icon"
/>
</>
);
};
const WithMenu = () => {
const [value, setValue] = React.useState<string>('');
return (
<SearchBar
value={value}
onChange={setValue}
onRequestSearch={action('request search')}
placeholder="Search with menu"
buildMenuTemplate={() => [
{
type: 'checkbox',
label: 'Tag 1',
checked: false,
click: action('Clicked Tag 1'),
},
{
type: 'checkbox',
label: 'Tag 2 (checked)',
checked: true,
click: action('Clicked Tag 2 (checked)'),
},
{
type: 'checkbox',
label: 'Tag 3',
checked: false,
click: action('Clicked Tag 3'),
},
]}
/>
<>
<Text>Value in state is: "{value}".</Text>
<SearchBar
value={value}
onChange={setValue}
onRequestSearch={action('request search')}
placeholder="Search with menu"
buildMenuTemplate={() => [
{
type: 'checkbox',
label: 'Tag 1',
checked: false,
click: action('Clicked Tag 1'),
},
{
type: 'checkbox',
label: 'Tag 2 (checked)',
checked: true,
click: action('Clicked Tag 2 (checked)'),
},
{
type: 'checkbox',
label: 'Tag 3',
checked: false,
click: action('Clicked Tag 3'),
},
]}
/>
</>
);
};
@@ -113,18 +132,21 @@ const WithTags = () => {
const [value, setValue] = React.useState<string>('');
const filtersState = useFilters();
return (
<SearchBar
value={value}
onChange={setValue}
onRequestSearch={action('request search')}
placeholder="Search with tags"
tagsHandler={{
add: filtersState.addFilter,
remove: filtersState.removeFilter,
chosenTags: filtersState.chosenFilters,
}}
tags={['Platformer', 'RPG', 'Beat them all', 'Top down']}
/>
<>
<Text>Value in state is: "{value}".</Text>
<SearchBar
value={value}
onChange={setValue}
onRequestSearch={action('request search')}
placeholder="Search with tags"
tagsHandler={{
add: filtersState.addFilter,
remove: filtersState.removeFilter,
chosenTags: filtersState.chosenFilters,
}}
tags={['Platformer', 'RPG', 'Beat them all', 'Top down']}
/>
</>
);
};
@@ -132,19 +154,22 @@ const WithTagsAndHelp = () => {
const [value, setValue] = React.useState<string>('');
const filtersState = useFilters();
return (
<SearchBar
value={value}
onChange={setValue}
onRequestSearch={action('request search')}
placeholder="Search with tags and help"
tagsHandler={{
add: filtersState.addFilter,
remove: filtersState.removeFilter,
chosenTags: filtersState.chosenFilters,
}}
tags={['Platformer', 'RPG', 'Beat them all', 'Top down']}
helpPagePath="https://gdevelop.io"
/>
<>
<Text>Value in state is: "{value}".</Text>
<SearchBar
value={value}
onChange={setValue}
onRequestSearch={action('request search')}
placeholder="Search with tags and help"
tagsHandler={{
add: filtersState.addFilter,
remove: filtersState.removeFilter,
chosenTags: filtersState.chosenFilters,
}}
tags={['Platformer', 'RPG', 'Beat them all', 'Top down']}
helpPagePath="https://gdevelop.io"
/>
</>
);
};