From 4f691d27b24e9e4f7ef3f3a30d91f4e1649620e9 Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 27 Nov 2025 15:23:43 +0200 Subject: [PATCH 1/2] feat: persist search query in URL parameters on projects page Fixes #3101 --- .../components/dashboard/projects/show.tsx | 79 +++++++++++++++++-- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 5369a544e..f1df2b9ef 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -10,7 +10,8 @@ import { TrashIcon, } from "lucide-react"; import Link from "next/link"; -import { useEffect, useMemo, useState } from "react"; +import { useRouter } from "next/router"; +import { useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { DateTooltip } from "@/components/shared/date-tooltip"; @@ -54,16 +55,27 @@ import { } from "@/components/ui/select"; import { TimeBadge } from "@/components/ui/time-badge"; import { api } from "@/utils/api"; +import { useDebounce } from "@/utils/hooks/use-debounce"; import { HandleProject } from "./handle-project"; import { ProjectEnvironment } from "./project-environment"; export const ShowProjects = () => { const utils = api.useUtils(); + const router = useRouter(); const { data: isCloud } = api.settings.isCloud.useQuery(); const { data, isLoading } = api.project.all.useQuery(); const { data: auth } = api.user.get.useQuery(); const { mutateAsync } = api.project.remove.useMutation(); - const [searchQuery, setSearchQuery] = useState(""); + const [searchQuery, setSearchQuery] = useState(() => { + // Initialize from URL query parameter on mount + if (typeof window !== "undefined") { + const urlQuery = router.query.q; + return typeof urlQuery === "string" ? urlQuery : ""; + } + return ""; + }); + const debouncedSearchQuery = useDebounce(searchQuery, 500); + const isUrlSync = useRef(false); const [sortBy, setSortBy] = useState(() => { if (typeof window !== "undefined") { return localStorage.getItem("projectsSort") || "createdAt-desc"; @@ -75,14 +87,71 @@ export const ShowProjects = () => { localStorage.setItem("projectsSort", sortBy); }, [sortBy]); + // Sync URL changes back to state (e.g., browser back/forward) + // NOTE: This effect MUST appear before the URL sync effect below + // because React executes effects in order. When the back button is pressed, + // we need to set isUrlSync.current = true BEFORE the URL sync effect runs. + useEffect(() => { + if (!router.isReady) return; + + const urlQuery = router.query.q; + const urlSearchQuery = typeof urlQuery === "string" ? urlQuery : ""; + + // Update local state if URL changed externally (e.g., browser navigation) + if (urlSearchQuery !== searchQuery) { + isUrlSync.current = true; + setSearchQuery(urlSearchQuery); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [router.isReady, router.query.q]); + + // Sync search query to URL when debounced value changes + useEffect(() => { + if (!router.isReady) return; + + // Skip if this change came from URL navigation (browser back/forward) + if (isUrlSync.current) { + isUrlSync.current = false; + return; + } + + const currentQuery = router.query.q; + const normalizedCurrentQuery = + typeof currentQuery === "string" ? currentQuery : ""; + + // Only update URL if the debounced value is different from current URL + if (debouncedSearchQuery !== normalizedCurrentQuery) { + const newQuery = { ...router.query }; + + if (debouncedSearchQuery) { + newQuery.q = debouncedSearchQuery; + } else { + delete newQuery.q; + } + + router.push( + { + pathname: router.pathname, + query: newQuery, + }, + undefined, + { shallow: false }, + ); + } + }, [debouncedSearchQuery, router]); + const filteredProjects = useMemo(() => { if (!data) return []; // First filter by search query const filtered = data.filter( (project) => - project.name.toLowerCase().includes(searchQuery.toLowerCase()) || - project.description?.toLowerCase().includes(searchQuery.toLowerCase()), + project.name + .toLowerCase() + .includes(debouncedSearchQuery.toLowerCase()) || + project.description + ?.toLowerCase() + .includes(debouncedSearchQuery.toLowerCase()), ); // Then sort the filtered results @@ -130,7 +199,7 @@ export const ShowProjects = () => { } return direction === "asc" ? comparison : -comparison; }); - }, [data, searchQuery, sortBy]); + }, [data, debouncedSearchQuery, sortBy]); return ( <> From 0f6ac310b5b00a894ee9c71311b0c6bd6eae01d8 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 7 Dec 2025 20:21:02 -0600 Subject: [PATCH 2/2] refactor(dashboard): streamline search query handling and URL synchronization in project display --- .../components/dashboard/projects/show.tsx | 74 +++++-------------- 1 file changed, 20 insertions(+), 54 deletions(-) diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 086174e41..5f7469050 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -11,7 +11,7 @@ import { } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/router"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { DateTooltip } from "@/components/shared/date-tooltip"; @@ -66,16 +66,12 @@ export const ShowProjects = () => { const { data, isLoading } = api.project.all.useQuery(); const { data: auth } = api.user.get.useQuery(); const { mutateAsync } = api.project.remove.useMutation(); - const [searchQuery, setSearchQuery] = useState(() => { - // Initialize from URL query parameter on mount - if (typeof window !== "undefined") { - const urlQuery = router.query.q; - return typeof urlQuery === "string" ? urlQuery : ""; - } - return ""; - }); + + const [searchQuery, setSearchQuery] = useState( + router.isReady && typeof router.query.q === "string" ? router.query.q : "", + ); const debouncedSearchQuery = useDebounce(searchQuery, 500); - const isUrlSync = useRef(false); + const [sortBy, setSortBy] = useState(() => { if (typeof window !== "undefined") { return localStorage.getItem("projectsSort") || "createdAt-desc"; @@ -87,63 +83,33 @@ export const ShowProjects = () => { localStorage.setItem("projectsSort", sortBy); }, [sortBy]); - // Sync URL changes back to state (e.g., browser back/forward) - // NOTE: This effect MUST appear before the URL sync effect below - // because React executes effects in order. When the back button is pressed, - // we need to set isUrlSync.current = true BEFORE the URL sync effect runs. useEffect(() => { if (!router.isReady) return; - - const urlQuery = router.query.q; - const urlSearchQuery = typeof urlQuery === "string" ? urlQuery : ""; - - // Update local state if URL changed externally (e.g., browser navigation) - if (urlSearchQuery !== searchQuery) { - isUrlSync.current = true; - setSearchQuery(urlSearchQuery); + const urlQuery = typeof router.query.q === "string" ? router.query.q : ""; + if (urlQuery !== searchQuery) { + setSearchQuery(urlQuery); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [router.isReady, router.query.q]); - // Sync search query to URL when debounced value changes useEffect(() => { if (!router.isReady) return; + const urlQuery = typeof router.query.q === "string" ? router.query.q : ""; + if (debouncedSearchQuery === urlQuery) return; - // Skip if this change came from URL navigation (browser back/forward) - if (isUrlSync.current) { - isUrlSync.current = false; - return; + const newQuery = { ...router.query }; + if (debouncedSearchQuery) { + newQuery.q = debouncedSearchQuery; + } else { + delete newQuery.q; } - - const currentQuery = router.query.q; - const normalizedCurrentQuery = - typeof currentQuery === "string" ? currentQuery : ""; - - // Only update URL if the debounced value is different from current URL - if (debouncedSearchQuery !== normalizedCurrentQuery) { - const newQuery = { ...router.query }; - - if (debouncedSearchQuery) { - newQuery.q = debouncedSearchQuery; - } else { - delete newQuery.q; - } - - router.push( - { - pathname: router.pathname, - query: newQuery, - }, - undefined, - { shallow: false }, - ); - } - }, [debouncedSearchQuery, router]); + router.replace({ pathname: router.pathname, query: newQuery }, undefined, { + shallow: true, + }); + }, [debouncedSearchQuery]); const filteredProjects = useMemo(() => { if (!data) return []; - // First filter by search query const filtered = data.filter( (project) => project.name