mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add module filter tabs
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { PackageIcon, SearchIcon } from "lucide-react";
|
||||
import { type FC, type PropsWithChildren, useMemo, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { templateBuilderModules } from "#/api/queries/templateBuilder";
|
||||
@@ -9,6 +10,8 @@ import { Alert, AlertDescription, AlertTitle } from "#/components/Alert/Alert";
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import { SearchField } from "#/components/SearchField/SearchField";
|
||||
import { Tabs, TabsList, TabsTrigger } from "#/components/Tabs/Tabs";
|
||||
import { useKebabMenu } from "#/components/Tabs/utils/useKebabMenu";
|
||||
import { useFuzzySearch } from "#/hooks/useFuzzySearch";
|
||||
import { ModuleCard } from "./ModuleCard";
|
||||
import {
|
||||
@@ -72,8 +75,30 @@ export const ModuleSelectStep: FC<ModuleSelectStepProps> = ({
|
||||
const [moduleSearchText, setModuleSearchText] = useState("");
|
||||
const modules = data?.modules ?? [];
|
||||
|
||||
const [selectedFilterTab, setSelectedFilterTab] = useState("All");
|
||||
// TODO category counts should update on search
|
||||
const categoryCounts = new Map<string, number>();
|
||||
for (const module of modules) {
|
||||
categoryCounts.set(
|
||||
module.category,
|
||||
(categoryCounts.get(module.category) ?? 0) + 1,
|
||||
);
|
||||
}
|
||||
const filterTabs: Array<{ value: string; count: number }> = [
|
||||
{ value: "All", count: modules.length },
|
||||
...categoryCounts.entries().map(([value, count]) => ({ value, count })),
|
||||
];
|
||||
const { containerRef, visibleTabs: visibleFilterTabs } = useKebabMenu({
|
||||
tabs: filterTabs,
|
||||
enabled: true,
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const searchedModules = useFuzzySearch({
|
||||
allItems: modules,
|
||||
allItems:
|
||||
selectedFilterTab === "All"
|
||||
? modules
|
||||
: modules.filter((module) => module.category === selectedFilterTab),
|
||||
searchText: moduleSearchText,
|
||||
searchProperties: ["display_name", "description"],
|
||||
});
|
||||
@@ -154,18 +179,42 @@ export const ModuleSelectStep: FC<ModuleSelectStepProps> = ({
|
||||
className="my-4"
|
||||
/>
|
||||
|
||||
<Tabs
|
||||
value={selectedFilterTab}
|
||||
onValueChange={setSelectedFilterTab}
|
||||
className="my-4"
|
||||
>
|
||||
<TabsList ref={containerRef}>
|
||||
{visibleFilterTabs.map((tab) => (
|
||||
<TabsTrigger key={tab.value} value={tab.value}>
|
||||
<PackageIcon className="size-icon-sm" />
|
||||
{tab.value} ({tab.count})
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{searchedModules.map((m) => (
|
||||
<ModuleCard
|
||||
key={m.id}
|
||||
name={m.display_name}
|
||||
description={m.description}
|
||||
iconUrl={m.icon}
|
||||
detailsUrl={moduleDetailsUrl(m.id)}
|
||||
selected={selectedSet.has(m.id)}
|
||||
onSelect={() => handleToggle(m)}
|
||||
/>
|
||||
))}
|
||||
{searchedModules.length ? (
|
||||
searchedModules.map((m) => (
|
||||
<ModuleCard
|
||||
key={m.id}
|
||||
name={m.display_name}
|
||||
description={m.description}
|
||||
iconUrl={m.icon}
|
||||
detailsUrl={moduleDetailsUrl(m.id)}
|
||||
selected={selectedSet.has(m.id)}
|
||||
onSelect={() => handleToggle(m)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-full my-12 flex flex-col items-center gap-1 text-content-secondary">
|
||||
<SearchIcon />
|
||||
<p className="m-0 text-sm font-normal">
|
||||
No module matched your search
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{conflicts.length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user