Add search footer to layout, behind a feature flag
Yeah it looks cute as a starting point! Definitely a lot to do here tho 😳
This commit is contained in:
parent
59fe02a4cc
commit
ea33741594
4 changed files with 202 additions and 133 deletions
46
src/app/WardrobePage/SearchFooter.js
Normal file
46
src/app/WardrobePage/SearchFooter.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import React from "react";
|
||||||
|
import * as Sentry from "@sentry/react";
|
||||||
|
import { Box, Flex } from "@chakra-ui/react";
|
||||||
|
import SearchToolbar, { emptySearchQuery } from "./SearchToolbar";
|
||||||
|
import { MajorErrorMessage, TestErrorSender, useLocalStorage } from "../util";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SearchFooter appears on large screens only, to let you search for new items
|
||||||
|
* while still keeping the rest of the item screen open!
|
||||||
|
*/
|
||||||
|
function SearchFooter() {
|
||||||
|
const [canUseSearchFooter, setCanUseSearchFooter] = useLocalStorage(
|
||||||
|
"DTIFeatureFlagCanUseSearchFooter",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (window.location.search.includes("feature-flag-can-use-search-footer")) {
|
||||||
|
setCanUseSearchFooter(true);
|
||||||
|
}
|
||||||
|
}, [setCanUseSearchFooter]);
|
||||||
|
|
||||||
|
const [query, setQuery] = React.useState(emptySearchQuery);
|
||||||
|
|
||||||
|
// TODO: Show the new footer to other users, too!
|
||||||
|
if (!canUseSearchFooter) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box paddingX="4" paddingY="4">
|
||||||
|
<Sentry.ErrorBoundary fallback={MajorErrorMessage}>
|
||||||
|
<TestErrorSender />
|
||||||
|
<Flex as="label" align="center">
|
||||||
|
<Box fontWeight="600" flex="0 0 auto">
|
||||||
|
Add new items:
|
||||||
|
</Box>
|
||||||
|
<Box width="4" />
|
||||||
|
<SearchToolbar query={query} onChange={setQuery} flex="0 1 100%" />
|
||||||
|
</Flex>
|
||||||
|
</Sentry.ErrorBoundary>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchFooter;
|
|
@ -53,6 +53,7 @@ function SearchToolbar({
|
||||||
showItemsLabel = false,
|
showItemsLabel = false,
|
||||||
background = null,
|
background = null,
|
||||||
boxShadow = null,
|
boxShadow = null,
|
||||||
|
...props
|
||||||
}) {
|
}) {
|
||||||
const [suggestions, setSuggestions] = React.useState([]);
|
const [suggestions, setSuggestions] = React.useState([]);
|
||||||
const [advancedSearchIsOpen, setAdvancedSearchIsOpen] = React.useState(false);
|
const [advancedSearchIsOpen, setAdvancedSearchIsOpen] = React.useState(false);
|
||||||
|
@ -182,139 +183,144 @@ function SearchToolbar({
|
||||||
const focusBorderColor = useColorModeValue("green.600", "green.400");
|
const focusBorderColor = useColorModeValue("green.600", "green.400");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Autosuggest
|
<Box {...props}>
|
||||||
suggestions={advancedSearchIsOpen ? allSuggestions : suggestions}
|
<Autosuggest
|
||||||
onSuggestionsFetchRequested={({ value }) => {
|
suggestions={advancedSearchIsOpen ? allSuggestions : suggestions}
|
||||||
// HACK: I'm not sure why, but apparently this gets called with value
|
onSuggestionsFetchRequested={({ value }) => {
|
||||||
// set to the _chosen suggestion_ after choosing it? Has that
|
// HACK: I'm not sure why, but apparently this gets called with value
|
||||||
// always happened? Idk? Let's just, gate around it, I guess?
|
// set to the _chosen suggestion_ after choosing it? Has that
|
||||||
if (typeof value === "string") {
|
// always happened? Idk? Let's just, gate around it, I guess?
|
||||||
setSuggestions(getSuggestions(value, query, zoneLabels, isLoggedIn));
|
if (typeof value === "string") {
|
||||||
}
|
setSuggestions(
|
||||||
}}
|
getSuggestions(value, query, zoneLabels, isLoggedIn)
|
||||||
onSuggestionSelected={(e, { suggestion }) => {
|
);
|
||||||
onChange({
|
}
|
||||||
...query,
|
}}
|
||||||
// If the suggestion was from typing, remove the last word of the
|
onSuggestionSelected={(e, { suggestion }) => {
|
||||||
// query value. Or, if it was from Advanced Search, leave it alone!
|
onChange({
|
||||||
value: advancedSearchIsOpen
|
...query,
|
||||||
? query.value
|
// If the suggestion was from typing, remove the last word of the
|
||||||
: removeLastWord(query.value),
|
// query value. Or, if it was from Advanced Search, leave it alone!
|
||||||
filterToZoneLabel: suggestion.zoneLabel || query.filterToZoneLabel,
|
value: advancedSearchIsOpen
|
||||||
filterToItemKind: suggestion.itemKind || query.filterToItemKind,
|
? query.value
|
||||||
filterToCurrentUserOwnsOrWants:
|
: removeLastWord(query.value),
|
||||||
suggestion.userOwnsOrWants || query.filterToCurrentUserOwnsOrWants,
|
filterToZoneLabel: suggestion.zoneLabel || query.filterToZoneLabel,
|
||||||
});
|
filterToItemKind: suggestion.itemKind || query.filterToItemKind,
|
||||||
}}
|
filterToCurrentUserOwnsOrWants:
|
||||||
getSuggestionValue={(zl) => zl}
|
suggestion.userOwnsOrWants ||
|
||||||
alwaysRenderSuggestions={true}
|
query.filterToCurrentUserOwnsOrWants,
|
||||||
renderSuggestion={renderSuggestion}
|
});
|
||||||
renderSuggestionsContainer={renderSuggestionsContainer}
|
}}
|
||||||
renderInputComponent={(inputProps) => (
|
getSuggestionValue={(zl) => zl}
|
||||||
<InputGroup boxShadow={boxShadow} borderRadius="md">
|
alwaysRenderSuggestions={true}
|
||||||
{queryFilterText ? (
|
renderSuggestion={renderSuggestion}
|
||||||
<InputLeftAddon>
|
renderSuggestionsContainer={renderSuggestionsContainer}
|
||||||
<SearchIcon color="gray.400" marginRight="3" />
|
renderInputComponent={(inputProps) => (
|
||||||
<Box fontSize="sm">{queryFilterText}</Box>
|
<InputGroup boxShadow={boxShadow} borderRadius="md">
|
||||||
</InputLeftAddon>
|
{queryFilterText ? (
|
||||||
) : (
|
<InputLeftAddon>
|
||||||
<InputLeftElement>
|
<SearchIcon color="gray.400" marginRight="3" />
|
||||||
<SearchIcon color="gray.400" />
|
<Box fontSize="sm">{queryFilterText}</Box>
|
||||||
</InputLeftElement>
|
</InputLeftAddon>
|
||||||
)}
|
) : (
|
||||||
<Input
|
<InputLeftElement>
|
||||||
background={background}
|
<SearchIcon color="gray.400" />
|
||||||
autoFocus={autoFocus}
|
</InputLeftElement>
|
||||||
{...inputProps}
|
)}
|
||||||
/>
|
<Input
|
||||||
<InputRightElement
|
background={background}
|
||||||
width="auto"
|
autoFocus={autoFocus}
|
||||||
justifyContent="flex-end"
|
{...inputProps}
|
||||||
paddingRight="2px"
|
/>
|
||||||
paddingY="2px"
|
<InputRightElement
|
||||||
>
|
width="auto"
|
||||||
{!searchQueryIsEmpty(query) && (
|
justifyContent="flex-end"
|
||||||
<Tooltip label="Clear">
|
paddingRight="2px"
|
||||||
|
paddingY="2px"
|
||||||
|
>
|
||||||
|
{!searchQueryIsEmpty(query) && (
|
||||||
|
<Tooltip label="Clear">
|
||||||
|
<IconButton
|
||||||
|
icon={<CloseIcon fontSize="0.6em" />}
|
||||||
|
color="gray.400"
|
||||||
|
variant="ghost"
|
||||||
|
height="100%"
|
||||||
|
marginLeft="1"
|
||||||
|
aria-label="Clear search"
|
||||||
|
onClick={() => {
|
||||||
|
setSuggestions([]);
|
||||||
|
onChange(emptySearchQuery);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Tooltip label="Advanced search">
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<CloseIcon fontSize="0.6em" />}
|
icon={
|
||||||
|
advancedSearchIsOpen ? (
|
||||||
|
<ChevronUpIcon fontSize="1.5em" />
|
||||||
|
) : (
|
||||||
|
<ChevronDownIcon fontSize="1.5em" />
|
||||||
|
)
|
||||||
|
}
|
||||||
color="gray.400"
|
color="gray.400"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
height="100%"
|
height="100%"
|
||||||
marginLeft="1"
|
aria-label="Open advanced search"
|
||||||
aria-label="Clear search"
|
onClick={() => setAdvancedSearchIsOpen((isOpen) => !isOpen)}
|
||||||
onClick={() => {
|
|
||||||
setSuggestions([]);
|
|
||||||
onChange(emptySearchQuery);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
</InputRightElement>
|
||||||
<Tooltip label="Advanced search">
|
</InputGroup>
|
||||||
<IconButton
|
)}
|
||||||
icon={
|
inputProps={{
|
||||||
advancedSearchIsOpen ? (
|
placeholder: "Search all items…",
|
||||||
<ChevronUpIcon fontSize="1.5em" />
|
focusBorderColor: focusBorderColor,
|
||||||
) : (
|
value: query.value || "",
|
||||||
<ChevronDownIcon fontSize="1.5em" />
|
ref: searchQueryRef,
|
||||||
)
|
minWidth: 0,
|
||||||
}
|
"data-test-id": "item-search-input",
|
||||||
color="gray.400"
|
onChange: (e, { newValue, method }) => {
|
||||||
variant="ghost"
|
// The Autosuggest tries to change the _entire_ value of the element
|
||||||
height="100%"
|
// when navigating suggestions, which isn't actually what we want.
|
||||||
aria-label="Open advanced search"
|
// Only accept value changes that are typed by the user!
|
||||||
onClick={() => setAdvancedSearchIsOpen((isOpen) => !isOpen)}
|
if (method === "type") {
|
||||||
/>
|
onChange({ ...query, value: newValue });
|
||||||
</Tooltip>
|
|
||||||
</InputRightElement>
|
|
||||||
</InputGroup>
|
|
||||||
)}
|
|
||||||
inputProps={{
|
|
||||||
placeholder: "Search all items…",
|
|
||||||
focusBorderColor: focusBorderColor,
|
|
||||||
value: query.value || "",
|
|
||||||
ref: searchQueryRef,
|
|
||||||
minWidth: 0,
|
|
||||||
"data-test-id": "item-search-input",
|
|
||||||
onChange: (e, { newValue, method }) => {
|
|
||||||
// The Autosuggest tries to change the _entire_ value of the element
|
|
||||||
// when navigating suggestions, which isn't actually what we want.
|
|
||||||
// Only accept value changes that are typed by the user!
|
|
||||||
if (method === "type") {
|
|
||||||
onChange({ ...query, value: newValue });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onKeyDown: (e) => {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
if (suggestions.length > 0) {
|
|
||||||
setSuggestions([]);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
onChange(emptySearchQuery);
|
},
|
||||||
e.target.blur();
|
onKeyDown: (e) => {
|
||||||
} else if (e.key === "Enter") {
|
if (e.key === "Escape") {
|
||||||
// Pressing Enter doesn't actually submit because it's all on
|
if (suggestions.length > 0) {
|
||||||
// debounce, but it can be a declaration that the query is done, so
|
setSuggestions([]);
|
||||||
// filter suggestions should go away!
|
return;
|
||||||
if (suggestions.length > 0) {
|
}
|
||||||
setSuggestions([]);
|
onChange(emptySearchQuery);
|
||||||
return;
|
e.target.blur();
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
// Pressing Enter doesn't actually submit because it's all on
|
||||||
|
// debounce, but it can be a declaration that the query is done, so
|
||||||
|
// filter suggestions should go away!
|
||||||
|
if (suggestions.length > 0) {
|
||||||
|
setSuggestions([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (e.key === "ArrowDown") {
|
||||||
|
if (suggestions.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onMoveFocusDownToResults(e);
|
||||||
|
} else if (e.key === "Backspace" && e.target.selectionStart === 0) {
|
||||||
|
onChange({
|
||||||
|
...query,
|
||||||
|
filterToItemKind: null,
|
||||||
|
filterToZoneLabel: null,
|
||||||
|
filterToCurrentUserOwnsOrWants: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (e.key === "ArrowDown") {
|
},
|
||||||
if (suggestions.length > 0) {
|
}}
|
||||||
return;
|
/>
|
||||||
}
|
</Box>
|
||||||
onMoveFocusDownToResults(e);
|
|
||||||
} else if (e.key === "Backspace" && e.target.selectionStart === 0) {
|
|
||||||
onChange({
|
|
||||||
...query,
|
|
||||||
filterToItemKind: null,
|
|
||||||
filterToZoneLabel: null,
|
|
||||||
filterToCurrentUserOwnsOrWants: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Box, Grid, useColorModeValue } from "@chakra-ui/react";
|
import { Box, Grid, useColorModeValue, useToken } from "@chakra-ui/react";
|
||||||
|
import { useCommonStyles } from "../util";
|
||||||
|
|
||||||
function WardrobePageLayout({ previewAndControls, itemsAndSearch }) {
|
function WardrobePageLayout({
|
||||||
|
previewAndControls = null,
|
||||||
|
itemsAndMaybeSearchPanel = null,
|
||||||
|
searchFooter = null,
|
||||||
|
}) {
|
||||||
const itemsAndSearchBackground = useColorModeValue("white", "gray.900");
|
const itemsAndSearchBackground = useColorModeValue("white", "gray.900");
|
||||||
|
const searchBackground = useCommonStyles().bodyBackground;
|
||||||
|
const searchShadowColorValue = useToken("colors", "gray.400");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
@ -18,12 +25,13 @@ function WardrobePageLayout({ previewAndControls, itemsAndSearch }) {
|
||||||
<Grid
|
<Grid
|
||||||
templateAreas={{
|
templateAreas={{
|
||||||
base: `"previewAndControls"
|
base: `"previewAndControls"
|
||||||
"itemsAndSearch"`,
|
"itemsAndMaybeSearchPanel"`,
|
||||||
md: `"previewAndControls itemsAndSearch"`,
|
md: `"previewAndControls itemsAndMaybeSearchPanel"
|
||||||
|
"searchFooter searchFooter"`,
|
||||||
}}
|
}}
|
||||||
templateRows={{
|
templateRows={{
|
||||||
base: "minmax(100px, 45%) minmax(300px, 55%)",
|
base: "minmax(100px, 45%) minmax(300px, 55%)",
|
||||||
md: "100%",
|
md: "minmax(300px, 1fr) auto",
|
||||||
}}
|
}}
|
||||||
templateColumns={{
|
templateColumns={{
|
||||||
base: "100%",
|
base: "100%",
|
||||||
|
@ -40,8 +48,15 @@ function WardrobePageLayout({ previewAndControls, itemsAndSearch }) {
|
||||||
>
|
>
|
||||||
{previewAndControls}
|
{previewAndControls}
|
||||||
</Box>
|
</Box>
|
||||||
<Box gridArea="itemsAndSearch" bg={itemsAndSearchBackground}>
|
<Box gridArea="itemsAndMaybeSearchPanel" bg={itemsAndSearchBackground}>
|
||||||
{itemsAndSearch}
|
{itemsAndMaybeSearchPanel}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
gridArea="searchFooter"
|
||||||
|
bg={searchBackground}
|
||||||
|
boxShadow={`0 0 8px ${searchShadowColorValue}`}
|
||||||
|
>
|
||||||
|
{searchFooter}
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useToast } from "@chakra-ui/react";
|
||||||
import { loadable } from "../util";
|
import { loadable } from "../util";
|
||||||
|
|
||||||
import ItemsAndSearchPanels from "./ItemsAndSearchPanels";
|
import ItemsAndSearchPanels from "./ItemsAndSearchPanels";
|
||||||
|
import SearchFooter from "./SearchFooter";
|
||||||
import SupportOnly from "./support/SupportOnly";
|
import SupportOnly from "./support/SupportOnly";
|
||||||
import useOutfitSaving from "./useOutfitSaving";
|
import useOutfitSaving from "./useOutfitSaving";
|
||||||
import useOutfitState, { OutfitStateContext } from "./useOutfitState";
|
import useOutfitState, { OutfitStateContext } from "./useOutfitState";
|
||||||
|
@ -104,7 +105,7 @@ function WardrobePage() {
|
||||||
dispatchToOutfit={dispatchToOutfit}
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
itemsAndSearch={
|
itemsAndMaybeSearchPanel={
|
||||||
<ItemsAndSearchPanels
|
<ItemsAndSearchPanels
|
||||||
loading={loading}
|
loading={loading}
|
||||||
outfitState={outfitState}
|
outfitState={outfitState}
|
||||||
|
@ -112,6 +113,7 @@ function WardrobePage() {
|
||||||
dispatchToOutfit={dispatchToOutfit}
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
searchFooter={<SearchFooter />}
|
||||||
/>
|
/>
|
||||||
</OutfitStateContext.Provider>
|
</OutfitStateContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue