From 3d706da4573315174a61c695a6b6566331d3924a Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Fri, 15 May 2026 12:02:44 +0330 Subject: [PATCH] fix(projects): improve project access modal UI and UX --- .../projects/ProjectAccessModal.tsx | 453 ++++++++++++++---- src/pages/Projects.tsx | 1 + 2 files changed, 353 insertions(+), 101 deletions(-) diff --git a/src/components/projects/ProjectAccessModal.tsx b/src/components/projects/ProjectAccessModal.tsx index e47a6ed..217fd27 100644 --- a/src/components/projects/ProjectAccessModal.tsx +++ b/src/components/projects/ProjectAccessModal.tsx @@ -1,5 +1,19 @@ import { useEffect, useMemo, useState } from "react"; -import { CheckSquare, Filter, ShieldCheck, Square } from "lucide-react"; +import { + Briefcase, + CheckCheck, + CheckCircle2, + CheckSquare, + FolderTree, + Loader2, + Search, + ShieldAlert, + ShieldCheck, + Square, + UserRound, + Users, + X, +} from "lucide-react"; import { toast } from "sonner"; import { @@ -12,12 +26,14 @@ import { fetchWorkspaceMemberships, type WorkspaceMembership } from "../../api/w import { Modal } from "../Modal"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; +import { Select } from "../ui/Select"; type Labels = { title: string; description: string; close: string; member: string; + projects: string; loading: string; noMembers: string; noProjects: string; @@ -67,19 +83,37 @@ export function ProjectAccessModal({ const [loadingMembers, setLoadingMembers] = useState(false); const [selectedUserId, setSelectedUserId] = useState(""); const [projectItems, setProjectItems] = useState([]); - const [selectedUserName, setSelectedUserName] = useState(""); - const [selectedUserMobile, setSelectedUserMobile] = useState(""); const [loadingProjects, setLoadingProjects] = useState(false); const [searchQuery, setSearchQuery] = useState(""); + const [memberSearchQuery, setMemberSearchQuery] = useState(""); const [selectedClientId, setSelectedClientId] = useState(""); const [selectedProjectIds, setSelectedProjectIds] = useState([]); const [isSaving, setIsSaving] = useState(false); + const isRtl = + typeof document !== "undefined" && document.documentElement.dir === "rtl"; const manageableMembers = useMemo( () => members.filter((member) => member.is_active && MANAGEABLE_ROLES.has(member.role)), [members], ); + const filteredMembers = useMemo(() => { + const normalizedSearch = memberSearchQuery.trim().toLowerCase(); + const baseMembers = !normalizedSearch + ? manageableMembers + : manageableMembers.filter((member) => { + const memberName = getMemberName(member).toLowerCase(); + const memberMobile = member.user?.mobile?.toLowerCase() ?? ""; + return memberName.includes(normalizedSearch) || memberMobile.includes(normalizedSearch); + }); + + return [...baseMembers].sort((a, b) => { + if (a.user.id === selectedUserId) return -1; + if (b.user.id === selectedUserId) return 1; + return 0; + }); + }, [manageableMembers, memberSearchQuery, selectedUserId]); + const clientOptions = useMemo(() => { const map = new Map(); projectItems.forEach((item) => { @@ -102,9 +136,17 @@ export function ProjectAccessModal({ }); }, [projectItems, searchQuery, selectedClientId]); + const visibleProjectIds = useMemo(() => visibleProjects.map((item) => item.id), [visibleProjects]); + + const selectedVisibleCount = useMemo( + () => selectedProjectIds.filter((id) => visibleProjectIds.includes(id)).length, + [selectedProjectIds, visibleProjectIds], + ); + useEffect(() => { if (!isOpen) { setSearchQuery(""); + setMemberSearchQuery(""); setSelectedClientId(""); setSelectedProjectIds([]); return; @@ -147,8 +189,6 @@ export function ProjectAccessModal({ try { const response = await getProjectAccessState(workspaceId, selectedUserId); setProjectItems(response.items); - setSelectedUserName(response.user.name); - setSelectedUserMobile(response.user.mobile); setSelectedProjectIds([]); } catch { toast.error(labels.loadError); @@ -170,8 +210,7 @@ export function ProjectAccessModal({ }; const handleSelectAllVisible = () => { - const visibleIds = visibleProjects.map((item) => item.id); - setSelectedProjectIds((current) => Array.from(new Set([...current, ...visibleIds]))); + setSelectedProjectIds((current) => Array.from(new Set([...current, ...visibleProjectIds]))); }; const handleSelectClientProjects = () => { @@ -209,141 +248,353 @@ export function ProjectAccessModal({ } }; + const footer = ( + <> +
+
+ {selectedProjectIds.length} +
+
+ + + + + ); + return ( - {labels.close} - - } + maxWidth="max-w-7xl" + footer={footer} >
-

{labels.description}

+

+ {labels.description} +

-
-
- - +
+ {/* LEFT SIDE */} +
+ {/* Header */} +
+
+ + {labels.projects} +
-
-
{selectedUserName || "-"}
-
{selectedUserMobile || "-"}
+
+
+ + + setSearchQuery(event.target.value)} + placeholder={labels.searchPlaceholder} + className="w-full pl-10 pr-4 rtl:pl-4 rtl:pr-10 py-2.5 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white outline-none focus:ring-2 focus:ring-blue-500 transition-shadow" + /> +
+ + setSearchQuery(event.target.value)} - placeholder={labels.searchPlaceholder} - /> - + + + + + + + +
+ {selectedVisibleCount}/{visibleProjects.length} +
-
- - - - - -
- -
-
+ {/* Projects */} +
+
{loadingProjects ? ( -
{labels.loading}
+
+ + {labels.loading} +
) : visibleProjects.length === 0 ? ( -
{labels.noProjects}
+
+ {labels.noProjects} +
) : ( -
+
{visibleProjects.map((item) => { const isChecked = selectedProjectIds.includes(item.id); + return ( - + ); })}
)}
-
+
+ + {/* RIGHT SIDEBAR */} +
diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx index 0e3a1cf..69eb6cb 100644 --- a/src/pages/Projects.tsx +++ b/src/pages/Projects.tsx @@ -463,6 +463,7 @@ export const Projects: React.FC = () => { description: t.projects?.accessModalDescription || "Grant or revoke project access for workspace members.", close: t.actions?.cancel || "Close", member: t.projects?.accessMemberLabel || "Member", + projects: t.sidebar?.projects || "Projects", loading: t.loading || "Loading...", noMembers: t.projects?.accessNoMembers || "No eligible members were found.", noProjects: t.projects?.accessNoProjects || "No projects found.",