Skip to content
Snippets Groups Projects
Unverified Commit e8133575 authored by Alexander Polyankin's avatar Alexander Polyankin Committed by GitHub
Browse files

Filter picker search free input (#38907)

parent af138e85
No related branches found
No related tags found
No related merge requests found
......@@ -31,7 +31,7 @@ interface FilterValuePickerProps<T> {
interface FilterValuePickerOwnProps extends FilterValuePickerProps<string> {
placeholder: string;
shouldCreate: (query: string) => boolean;
canAddValue: (query: string) => boolean;
}
function FilterValuePicker({
......@@ -42,7 +42,7 @@ function FilterValuePicker({
placeholder,
autoFocus = false,
compact = false,
shouldCreate,
canAddValue,
onChange,
onFocus,
onBlur,
......@@ -88,7 +88,7 @@ function FilterValuePicker({
fieldValues={fieldData?.values ?? []}
selectedValues={selectedValues}
placeholder={t`Search by ${columnInfo.displayName}`}
nothingFound={t`No matching ${columnInfo.displayName} found.`}
canAddValue={canAddValue}
autoFocus={autoFocus}
onChange={onChange}
/>
......@@ -99,7 +99,7 @@ function FilterValuePicker({
<StaticValuePicker
selectedValues={selectedValues}
placeholder={placeholder}
shouldCreate={shouldCreate}
canAddValue={canAddValue}
autoFocus={autoFocus}
onChange={onChange}
onFocus={onFocus}
......@@ -113,7 +113,7 @@ export function StringFilterValuePicker({
values,
...props
}: FilterValuePickerProps<string>) {
const shouldCreate = (query: string) => {
const canAddValue = (query: string) => {
return query.trim().length > 0 && !values.includes(query);
};
......@@ -123,7 +123,7 @@ export function StringFilterValuePicker({
column={column}
values={values}
placeholder={isKeyColumn(column) ? t`Enter an ID` : t`Enter some text`}
shouldCreate={shouldCreate}
canAddValue={canAddValue}
/>
);
}
......@@ -134,7 +134,7 @@ export function NumberFilterValuePicker({
onChange,
...props
}: FilterValuePickerProps<number>) {
const shouldCreate = (query: string) => {
const canAddValue = (query: string) => {
const number = parseFloat(query);
return isFinite(number) && !values.includes(number);
};
......@@ -145,7 +145,7 @@ export function NumberFilterValuePicker({
column={column}
values={values.map(value => String(value))}
placeholder={isKeyColumn(column) ? t`Enter an ID` : t`Enter a number`}
shouldCreate={shouldCreate}
canAddValue={canAddValue}
onChange={newValue => onChange(newValue.map(value => parseFloat(value)))}
/>
);
......
......@@ -370,6 +370,65 @@ describe("StringFilterValuePicker", () => {
expect(onChange).toHaveBeenLastCalledWith(["a-test"]);
});
it("should allow free-form input without waiting for search results", async () => {
const { onChange } = await setupStringPicker({
query,
stageIndex,
column,
values: [],
searchValues: {
"a@b.com": createMockFieldValues({
field_id: PEOPLE.EMAIL,
values: [["testa@b.com"]],
}),
},
});
userEvent.type(screen.getByPlaceholderText("Search by Email"), "a@b.com");
userEvent.hover(screen.getByText("a@b.com"));
userEvent.click(screen.getByText("a@b.com"));
expect(onChange).toHaveBeenLastCalledWith(["a@b.com"]);
});
it("should not be able to create duplicates with free-form input", async () => {
const { onChange } = await setupStringPicker({
query,
stageIndex,
column,
values: ["a@b.com"],
searchValues: {
"a@b.com": createMockFieldValues({
field_id: PEOPLE.EMAIL,
values: [["testa@b.com"]],
}),
},
});
userEvent.type(screen.getByLabelText("Filter value"), "a@b.com");
expect(screen.getByText("a@b.com")).toBeInTheDocument();
expect(onChange).not.toHaveBeenCalled();
});
it("should not allow to create a value when there is the exact match in search results", async () => {
const { onChange } = await setupStringPicker({
query,
stageIndex,
column,
values: ["a@b.com"],
searchValues: {
"a@b.com": createMockFieldValues({
field_id: PEOPLE.EMAIL,
values: [["a@b.com"]],
}),
},
});
userEvent.type(screen.getByLabelText("Filter value"), "a@b.com");
act(() => jest.advanceTimersByTime(1000));
expect(screen.getByText("a@b.com")).toBeInTheDocument();
expect(onChange).not.toHaveBeenCalled();
});
});
describe("no values", () => {
......
......@@ -8,7 +8,7 @@ import type { FieldId, FieldValue } from "metabase-types/api";
import { getEffectiveOptions } from "../utils";
import { SEARCH_DEBOUNCE } from "./constants";
import { shouldSearch, getSearchValues } from "./utils";
import { shouldSearch, getSearchValues, getOptimisticOptions } from "./utils";
interface SearchValuePickerProps {
fieldId: FieldId;
......@@ -16,7 +16,7 @@ interface SearchValuePickerProps {
fieldValues: FieldValue[];
selectedValues: string[];
placeholder?: string;
nothingFound?: string;
canAddValue: (query: string) => boolean;
autoFocus?: boolean;
onChange: (newValues: string[]) => void;
}
......@@ -27,14 +27,14 @@ export function SearchValuePicker({
fieldValues: initialFieldValues,
selectedValues,
placeholder,
nothingFound,
canAddValue,
autoFocus,
onChange,
}: SearchValuePickerProps) {
const [searchValue, setSearchValue] = useState("");
const [searchQuery, setSearchQuery] = useState(searchValue);
const { value: fieldValues = initialFieldValues, loading } = useAsync(
const { value: fieldValues = initialFieldValues } = useAsync(
() => getSearchValues(fieldId, searchFieldId, searchQuery),
[fieldId, searchFieldId, searchQuery],
);
......@@ -57,16 +57,14 @@ export function SearchValuePicker({
}
};
const isSearched = searchQuery.length > 0 && !loading;
useDebounce(handleSearchTimeout, SEARCH_DEBOUNCE, [searchValue]);
return (
<MultiSelect
data={options}
data={getOptimisticOptions(options, searchValue, canAddValue)}
value={selectedValues}
searchValue={searchValue}
placeholder={placeholder}
nothingFound={isSearched ? nothingFound : null}
searchable
autoFocus={autoFocus}
aria-label={t`Filter value`}
......
import { MetabaseApi } from "metabase/services";
import type { SelectItem } from "metabase/ui";
import type { FieldId, FieldValue } from "metabase-types/api";
import { SEARCH_LIMIT } from "./constants";
......@@ -31,3 +32,16 @@ export function shouldSearch(
return !isExtensionOfLastSearch || hasMoreValues;
}
export function getOptimisticOptions(
options: SelectItem[],
searchValue: string,
canAddValue: (query: string) => boolean,
) {
const isValid = canAddValue(searchValue);
const isExisting = options.some(({ label }) => label === searchValue);
return isValid && !isExisting
? [{ value: searchValue, label: searchValue }, ...options]
: options;
}
......@@ -7,7 +7,7 @@ import { MultiSelect } from "metabase/ui";
interface StaticValuePickerProps {
selectedValues: string[];
placeholder?: string;
shouldCreate: (query: string) => boolean;
canAddValue: (query: string) => boolean;
autoFocus?: boolean;
onChange: (newValues: string[]) => void;
onFocus?: (event: FocusEvent<HTMLInputElement>) => void;
......@@ -17,7 +17,7 @@ interface StaticValuePickerProps {
export function StaticValuePicker({
selectedValues,
placeholder,
shouldCreate,
canAddValue,
autoFocus,
onChange,
onFocus,
......@@ -49,7 +49,7 @@ export function StaticValuePicker({
const handleSearchChange = (newSearchValue: string) => {
setSearchValue(newSearchValue);
const isValid = shouldCreate(newSearchValue);
const isValid = canAddValue(newSearchValue);
if (isValid) {
onChange?.([...lastValues, newSearchValue]);
} else {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment