Skip to content

Commit

Permalink
[UI v2] feat: Updates combobox filtering logic for automations (#16737)
Browse files Browse the repository at this point in the history
  • Loading branch information
devinvillarosa authored Jan 16, 2025
1 parent 8b49e3f commit 3b73ba3
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from "@/components/ui/form";
import { Skeleton } from "@/components/ui/skeleton";
import { useQuery } from "@tanstack/react-query";
import { useDeferredValue, useMemo, useState } from "react";
import { useFormContext } from "react-hook-form";

const INFER_AUTOMATION = {
Expand All @@ -44,37 +45,35 @@ const getButtonLabel = (
return INFER_AUTOMATION.name;
}
const automation = data?.find((automation) => automation.id === fieldValue);
if (automation?.name) {
if (automation) {
return automation.name;
}
return undefined;
};

/** Because ShadCN only filters by `value` and not by a specific field, we need to write custom logic to filter objects by id */
const filterAutomations = (
value: string | null,
search: string,
data: Array<Automation> | undefined,
) => {
const searchTerm = search.toLowerCase();
const automation = data?.find((automation) => automation.id === value);
if (!automation) {
return 0;
}
const automationName = automation.name.toLowerCase();
if (automationName.includes(searchTerm)) {
return 1;
}
return 0;
};

export const AutomationsSelectStateFields = ({
action,
index,
}: AutomationsSelectStateFieldsProps) => {
const [search, setSearch] = useState("");
const form = useFormContext<AutomationWizardSchema>();
const { data, isSuccess } = useQuery(buildListAutomationsQuery());

// nb: because automations API does not have filtering _like by name, do client-side filtering
const deferredSearch = useDeferredValue(search);
const filteredData = useMemo(() => {
if (!data) {
return [];
}
return data.filter((automation) =>
automation.name.toLowerCase().includes(deferredSearch.toLowerCase()),
);
}, [data, deferredSearch]);

const isInferredOptionFiltered = INFER_AUTOMATION.name
.toLowerCase()
.includes(deferredSearch.toLowerCase());

return (
<FormField
control={form.control}
Expand All @@ -89,30 +88,38 @@ export const AutomationsSelectStateFields = ({
selected={Boolean(buttonLabel)}
aria-label={`Select Automation to ${action}`}
>
{getButtonLabel(data, field.value) ?? "Select automation"}
{buttonLabel ?? "Select automation"}
</ComboboxTrigger>
<ComboboxContent
filter={(value, search) =>
filterAutomations(value, search, data)
}
>
<ComboboxCommandInput placeholder="Search for an automation..." />
<ComboboxContent>
<ComboboxCommandInput
value={search}
onValueChange={setSearch}
placeholder="Search for an automation..."
/>
<ComboboxCommandEmtpy>No automation found</ComboboxCommandEmtpy>
<ComboboxCommandList>
<ComboboxCommandGroup>
<ComboboxCommandItem
selected={field.value === INFER_AUTOMATION.value}
onSelect={field.onChange}
value={INFER_AUTOMATION.value}
>
{INFER_AUTOMATION.name}
</ComboboxCommandItem>
{isInferredOptionFiltered && (
<ComboboxCommandItem
selected={field.value === INFER_AUTOMATION.value}
onSelect={(value) => {
field.onChange(value);
setSearch("");
}}
value={INFER_AUTOMATION.value}
>
{INFER_AUTOMATION.name}
</ComboboxCommandItem>
)}
{isSuccess ? (
data.map((automation) => (
filteredData.map((automation) => (
<ComboboxCommandItem
key={automation.id}
selected={field.value === automation.id}
onSelect={field.onChange}
onSelect={(value) => {
field.onChange(value);
setSearch("");
}}
value={automation.id}
>
{automation.name}
Expand Down
23 changes: 18 additions & 5 deletions ui-v2/src/components/ui/combobox/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,34 @@ const ComboboxTrigger = ({
};

const ComboboxContent = ({
filter,
children,
}: {
filter?: (value: string, search: string, keywords?: string[]) => number;
children: React.ReactNode;
}) => {
return (
<PopoverContent fullWidth>
<Command filter={filter}>{children}</Command>
<Command shouldFilter={false}>{children}</Command>
</PopoverContent>
);
};

const ComboboxCommandInput = ({ placeholder }: { placeholder?: string }) => {
return <CommandInput placeholder={placeholder} className="h-9" />;
const ComboboxCommandInput = ({
value,
onValueChange,
placeholder,
}: {
value?: string;
onValueChange?: (value: string) => void;
placeholder?: string;
}) => {
return (
<CommandInput
value={value}
onValueChange={onValueChange}
placeholder={placeholder}
className="h-9"
/>
);
};

const ComboboxCommandList = ({ children }: { children: React.ReactNode }) => {
Expand Down
77 changes: 38 additions & 39 deletions ui-v2/src/components/ui/combobox/comboxbox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";

import { Automation } from "@/api/automations";
import { createFakeAutomation } from "@/mocks";
import { useState } from "react";
import { useDeferredValue, useMemo, useState } from "react";
import {
Combobox,
ComboboxCommandEmtpy,
Expand All @@ -27,13 +27,7 @@ const INFER_AUTOMATION = {
name: "Infer Automation" as const,
} as const;

const MOCK_DATA = [
createFakeAutomation(),
createFakeAutomation(),
createFakeAutomation(),
createFakeAutomation(),
createFakeAutomation(),
];
const MOCK_DATA = Array.from({ length: 5 }, createFakeAutomation);

const getButtonLabel = (data: Array<Automation>, fieldValue: string) => {
if (fieldValue === INFER_AUTOMATION.value) {
Expand All @@ -46,55 +40,60 @@ const getButtonLabel = (data: Array<Automation>, fieldValue: string) => {
return undefined;
};

/** Because ShadCN only filters by `value` and not by a specific field, we need to write custom logic to filter objects by id */
const filterAutomations = (
value: string,
search: string,
data: Array<Automation> | undefined,
) => {
const searchTerm = search.toLowerCase();
const automation = data?.find((automation) => automation.id === value);
if (!automation) {
return 0;
}
const automationName = automation.name.toLowerCase();
if (automationName.includes(searchTerm)) {
return 1;
}
return 0;
};

function ComboboxStory() {
const [search, setSearch] = useState("");
const [selectedAutomationId, setSelectedAutomationId] = useState<
typeof UNASSIGNED | (string & {})
>(INFER_AUTOMATION.value);

const deferredSearch = useDeferredValue(search);

const filteredData = useMemo(() => {
return MOCK_DATA.filter((automation) =>
automation.name.toLowerCase().includes(deferredSearch.toLowerCase()),
);
}, [deferredSearch]);

const isInferredOptionFiltered = INFER_AUTOMATION.name
.toLowerCase()
.includes(deferredSearch.toLowerCase());

const buttonLabel = getButtonLabel(MOCK_DATA, selectedAutomationId);

return (
<Combobox>
<ComboboxTrigger selected={Boolean(buttonLabel)}>
{buttonLabel ?? "Select automation"}
</ComboboxTrigger>
<ComboboxContent
filter={(value, search) => filterAutomations(value, search, MOCK_DATA)}
>
<ComboboxCommandInput placeholder="Search for an automation..." />
<ComboboxContent>
<ComboboxCommandInput
value={search}
onValueChange={setSearch}
placeholder="Search for an automation..."
/>
<ComboboxCommandEmtpy>No automation found</ComboboxCommandEmtpy>
<ComboboxCommandList>
<ComboboxCommandGroup>
<ComboboxCommandItem
selected={selectedAutomationId === INFER_AUTOMATION.value}
onSelect={setSelectedAutomationId}
value={INFER_AUTOMATION.value}
>
{INFER_AUTOMATION.name}
</ComboboxCommandItem>
{MOCK_DATA.map((automation) => (
{isInferredOptionFiltered && (
<ComboboxCommandItem
selected={selectedAutomationId === INFER_AUTOMATION.value}
onSelect={(value) => {
setSelectedAutomationId(value);
setSearch("");
}}
value={INFER_AUTOMATION.value}
>
{INFER_AUTOMATION.name}
</ComboboxCommandItem>
)}
{filteredData.map((automation) => (
<ComboboxCommandItem
key={automation.id}
selected={selectedAutomationId === automation.id}
onSelect={setSelectedAutomationId}
onSelect={(value) => {
setSelectedAutomationId(value);
setSearch("");
}}
value={automation.id}
>
{automation.name}
Expand Down

0 comments on commit 3b73ba3

Please sign in to comment.