mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(site): fix search menu for creating workspace and templates filter (#11674)
This commit is contained in:
@@ -32,6 +32,12 @@ import { Loader } from "components/Loader/Loader";
|
||||
import { useDebouncedFunction } from "hooks/debounce";
|
||||
import { useFilterMenu } from "./menu";
|
||||
import type { BaseOption } from "./options";
|
||||
import {
|
||||
Search,
|
||||
SearchEmpty,
|
||||
SearchInput,
|
||||
searchStyles,
|
||||
} from "components/Menu/Search";
|
||||
|
||||
export type PresetFilter = {
|
||||
name: string;
|
||||
@@ -489,7 +495,7 @@ export const FilterSearchMenu = <TOption extends BaseOption>({
|
||||
onQueryChange={menu.setQuery}
|
||||
renderOption={(option) => (
|
||||
<MenuItem
|
||||
key={option.label}
|
||||
key={option.value}
|
||||
selected={option.value === menu.selectedOption?.value}
|
||||
onClick={() => {
|
||||
menu.selectOption(option);
|
||||
@@ -576,7 +582,6 @@ function SearchMenu<TOption extends BaseOption>({
|
||||
}: SearchMenuProps<TOption>) {
|
||||
const menuListRef = useRef<HTMLUListElement>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Menu
|
||||
@@ -586,10 +591,7 @@ function SearchMenu<TOption extends BaseOption>({
|
||||
onQueryChange("");
|
||||
}}
|
||||
css={{
|
||||
"& .MuiPaper-root": {
|
||||
width: 320,
|
||||
padding: 0,
|
||||
},
|
||||
"& .MuiPaper-root": searchStyles.content,
|
||||
}}
|
||||
// Disabled this so when we clear the filter and do some sorting in the
|
||||
// search items it does not look strange. Github removes exit transitions
|
||||
@@ -606,44 +608,16 @@ function SearchMenu<TOption extends BaseOption>({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<li
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
paddingLeft: 16,
|
||||
height: 40,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<SearchOutlined
|
||||
css={{
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
tabIndex={-1}
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
<Search component="li">
|
||||
<SearchInput
|
||||
autoFocus
|
||||
value={query}
|
||||
ref={searchInputRef}
|
||||
onChange={(e) => {
|
||||
onQueryChange(e.target.value);
|
||||
}}
|
||||
css={{
|
||||
height: "100%",
|
||||
border: 0,
|
||||
background: "none",
|
||||
width: "100%",
|
||||
marginLeft: 16,
|
||||
outline: 0,
|
||||
"&::placeholder": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
</Search>
|
||||
|
||||
<li css={{ maxHeight: 480, overflowY: "auto" }}>
|
||||
<MenuList
|
||||
@@ -660,17 +634,7 @@ function SearchMenu<TOption extends BaseOption>({
|
||||
options.length > 0 ? (
|
||||
options.map(renderOption)
|
||||
) : (
|
||||
<div
|
||||
css={{
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
textAlign: "center",
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
>
|
||||
No results
|
||||
</div>
|
||||
<SearchEmpty />
|
||||
)
|
||||
) : (
|
||||
<Loader size={14} />
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import SearchOutlined from "@mui/icons-material/SearchOutlined";
|
||||
// eslint-disable-next-line no-restricted-imports -- use it to have the component prop
|
||||
import Box, { BoxProps } from "@mui/material/Box";
|
||||
import { Interpolation, Theme, useTheme } from "@mui/material/styles";
|
||||
import visuallyHidden from "@mui/utils/visuallyHidden";
|
||||
import { FC, HTMLAttributes, InputHTMLAttributes, forwardRef } from "react";
|
||||
|
||||
export const Search = forwardRef<HTMLElement, BoxProps>(
|
||||
({ children, ...boxProps }, ref) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
{...boxProps}
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
paddingLeft: 16,
|
||||
height: 40,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<SearchOutlined
|
||||
css={{
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
type SearchInputProps = InputHTMLAttributes<HTMLInputElement> & {
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
||||
({ label, ...inputProps }, ref) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<label css={{ ...visuallyHidden }} htmlFor={inputProps.id}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
ref={ref}
|
||||
tabIndex={-1}
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
css={{
|
||||
height: "100%",
|
||||
border: 0,
|
||||
background: "none",
|
||||
flex: 1,
|
||||
marginLeft: 16,
|
||||
outline: 0,
|
||||
"&::placeholder": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}}
|
||||
{...inputProps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const SearchEmpty: FC<HTMLAttributes<HTMLDivElement>> = ({
|
||||
children = "Not found",
|
||||
...props
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
textAlign: "center",
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const searchStyles = {
|
||||
content: {
|
||||
width: 320,
|
||||
padding: 0,
|
||||
borderRadius: 4,
|
||||
},
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
@@ -11,7 +11,6 @@ import AddIcon from "@mui/icons-material/AddOutlined";
|
||||
import OpenIcon from "@mui/icons-material/OpenInNewOutlined";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { OverflowY } from "components/OverflowY/OverflowY";
|
||||
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { SearchBox } from "./WorkspacesSearchBox";
|
||||
import {
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "components/Popover/Popover";
|
||||
import { SearchEmpty, searchStyles } from "components/Menu/Search";
|
||||
|
||||
const ICON_SIZE = 18;
|
||||
|
||||
@@ -43,17 +43,15 @@ export const WorkspacesButton: FC<WorkspacesButtonProps> = ({
|
||||
let emptyState: ReactNode = undefined;
|
||||
if (templates?.length === 0) {
|
||||
emptyState = (
|
||||
<EmptyState
|
||||
message="No templates yet"
|
||||
cta={
|
||||
<Link to="/templates" component={RouterLink}>
|
||||
Create one now.
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<SearchEmpty>
|
||||
No templates yet.{" "}
|
||||
<Link to="/templates" component={RouterLink}>
|
||||
Create one now.
|
||||
</Link>
|
||||
</SearchEmpty>
|
||||
);
|
||||
} else if (processed.length === 0) {
|
||||
emptyState = <EmptyState message="No templates match your text" />;
|
||||
emptyState = <SearchEmpty>No templates found</SearchEmpty>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -63,7 +61,12 @@ export const WorkspacesButton: FC<WorkspacesButtonProps> = ({
|
||||
{children}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent horizontal="right">
|
||||
<PopoverContent
|
||||
horizontal="right"
|
||||
css={{
|
||||
".MuiPaper-root": searchStyles.content,
|
||||
}}
|
||||
>
|
||||
<SearchBox
|
||||
value={searchTerm}
|
||||
onValueChange={(newValue) => setSearchTerm(newValue)}
|
||||
|
||||
@@ -11,9 +11,7 @@ import {
|
||||
forwardRef,
|
||||
useId,
|
||||
} from "react";
|
||||
import SearchIcon from "@mui/icons-material/SearchOutlined";
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Search, SearchInput } from "components/Menu/Search";
|
||||
|
||||
interface SearchBoxProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
@@ -35,39 +33,12 @@ export const SearchBox = forwardRef(function SearchBox(
|
||||
} = props;
|
||||
|
||||
const hookId = useId();
|
||||
const theme = useTheme();
|
||||
|
||||
const inputId = `${hookId}-${SearchBox.name}-input`;
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
flexFlow: "row nowrap",
|
||||
alignItems: "center",
|
||||
padding: "0 8px",
|
||||
height: "40px",
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<div css={{ width: 18 }}>
|
||||
<SearchIcon
|
||||
css={{
|
||||
display: "block",
|
||||
fontSize: "14px",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label css={{ ...visuallyHidden }} htmlFor={inputId}>
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
<Search>
|
||||
<SearchInput
|
||||
label={label}
|
||||
ref={ref}
|
||||
id={inputId}
|
||||
autoFocus
|
||||
@@ -76,17 +47,7 @@ export const SearchBox = forwardRef(function SearchBox(
|
||||
{...attrs}
|
||||
onKeyDown={onKeyDown}
|
||||
onChange={(e) => onValueChange(e.target.value)}
|
||||
css={{
|
||||
height: "100%",
|
||||
border: 0,
|
||||
background: "none",
|
||||
width: "100%",
|
||||
outline: 0,
|
||||
"&::placeholder": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Search>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user