Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to .ts to resolve jsx re-rendering warning

File renamed without changes.
Loading