mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
Merge pull request #2319 from Dokploy/1614-sort-project-alphabetically-and-by-other-criteria
feat(show-projects): implement project sorting and filtering function…
This commit is contained in:
@@ -1,3 +1,17 @@
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowUpDown,
|
||||
BookIcon,
|
||||
ExternalLinkIcon,
|
||||
FolderInput,
|
||||
Loader2,
|
||||
MoreHorizontalIcon,
|
||||
Search,
|
||||
TrashIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
@@ -31,20 +45,14 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
AlertTriangle,
|
||||
BookIcon,
|
||||
ExternalLinkIcon,
|
||||
FolderInput,
|
||||
Loader2,
|
||||
MoreHorizontalIcon,
|
||||
Search,
|
||||
TrashIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { HandleProject } from "./handle-project";
|
||||
import { ProjectEnvironment } from "./project-environment";
|
||||
|
||||
@@ -54,15 +62,65 @@ export const ShowProjects = () => {
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { mutateAsync } = api.project.remove.useMutation();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [sortBy, setSortBy] = useState<string>(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
return localStorage.getItem("projectsSort") || "createdAt-desc";
|
||||
}
|
||||
return "createdAt-desc";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("projectsSort", sortBy);
|
||||
}, [sortBy]);
|
||||
|
||||
const filteredProjects = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return data.filter(
|
||||
|
||||
// First filter by search query
|
||||
const filtered = data.filter(
|
||||
(project) =>
|
||||
project.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
project.description?.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
);
|
||||
}, [data, searchQuery]);
|
||||
|
||||
// Then sort the filtered results
|
||||
const [field, direction] = sortBy.split("-");
|
||||
return [...filtered].sort((a, b) => {
|
||||
let comparison = 0;
|
||||
switch (field) {
|
||||
case "name":
|
||||
comparison = a.name.localeCompare(b.name);
|
||||
break;
|
||||
case "createdAt":
|
||||
comparison =
|
||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||
break;
|
||||
case "services": {
|
||||
const aTotalServices =
|
||||
a.mariadb.length +
|
||||
a.mongo.length +
|
||||
a.mysql.length +
|
||||
a.postgres.length +
|
||||
a.redis.length +
|
||||
a.applications.length +
|
||||
a.compose.length;
|
||||
const bTotalServices =
|
||||
b.mariadb.length +
|
||||
b.mongo.length +
|
||||
b.mysql.length +
|
||||
b.postgres.length +
|
||||
b.redis.length +
|
||||
b.applications.length +
|
||||
b.compose.length;
|
||||
comparison = aTotalServices - bTotalServices;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
comparison = 0;
|
||||
}
|
||||
return direction === "asc" ? comparison : -comparison;
|
||||
});
|
||||
}, [data, searchQuery, sortBy]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -98,14 +156,40 @@ export const ShowProjects = () => {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-full relative">
|
||||
<Input
|
||||
placeholder="Filter projects..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pr-10"
|
||||
/>
|
||||
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
<div className="flex max-sm:flex-col gap-4 items-center w-full">
|
||||
<div className="flex-1 relative max-sm:w-full">
|
||||
<Input
|
||||
placeholder="Filter projects..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pr-10"
|
||||
/>
|
||||
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 min-w-48 max-sm:w-full">
|
||||
<ArrowUpDown className="size-4 text-muted-foreground" />
|
||||
<Select value={sortBy} onValueChange={setSortBy}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Sort by..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
|
||||
<SelectItem value="name-desc">Name (Z-A)</SelectItem>
|
||||
<SelectItem value="createdAt-desc">
|
||||
Newest first
|
||||
</SelectItem>
|
||||
<SelectItem value="createdAt-asc">
|
||||
Oldest first
|
||||
</SelectItem>
|
||||
<SelectItem value="services-desc">
|
||||
Most services
|
||||
</SelectItem>
|
||||
<SelectItem value="services-asc">
|
||||
Least services
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{filteredProjects?.length === 0 && (
|
||||
<div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4">
|
||||
|
||||
Reference in New Issue
Block a user