From 0491b668c75719e64a381e199d39dc5b558062e8 Mon Sep 17 00:00:00 2001 From: ZhenYi <434836402@qq.com> Date: Fri, 15 May 2026 13:11:01 +0800 Subject: [PATCH] feat(project): add member invitation to project creation modal - Add email invite flow with role selection to ProjectCreateMenuModal - Update MembersSettings with avatar URL support - Adjust ProjectSettingsLayout layout for new content --- .../components/ProjectCreateMenuModal.tsx | 98 ++++++++++++++++++- src/app/project/settings/MembersSettings.tsx | 7 +- .../settings/ProjectSettingsLayout.tsx | 3 +- 3 files changed, 99 insertions(+), 9 deletions(-) diff --git a/src/app/project/components/ProjectCreateMenuModal.tsx b/src/app/project/components/ProjectCreateMenuModal.tsx index 6e019f7..08d4a0d 100644 --- a/src/app/project/components/ProjectCreateMenuModal.tsx +++ b/src/app/project/components/ProjectCreateMenuModal.tsx @@ -4,6 +4,8 @@ import { useCreateRepoMutation } from "@/hooks/useReposQuery"; import { useCreateRoomMutation } from "@/hooks/useRoomsQuery"; import { useBoardOperations } from "@/hooks/useBoardOperations"; import { useCreateSkillMutation } from "@/hooks/useSkillsQuery"; +import { projectInviteUser } from "@/client/api"; +import type { MemberRole } from "@/client/model"; import { X, Hash, @@ -15,23 +17,33 @@ import { FolderPlus, MessageSquarePlus, Kanban, - Zap + Zap, + Mail, + UserPlus, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; interface ProjectCreateMenuModalProps { onClose: () => void; - initialTab?: "repo" | "channel" | "board" | "skill"; + initialTab?: "repo" | "channel" | "board" | "skill" | "invite"; } export function ProjectCreateMenuModal({ onClose, initialTab = "repo" }: ProjectCreateMenuModalProps) { const { projectName } = useParams<{ projectName: string }>(); const navigate = useNavigate(); - const [activeTab, setActiveTab] = useState<"repo" | "channel" | "board" | "skill">(initialTab); + const [activeTab, setActiveTab] = useState<"repo" | "channel" | "board" | "skill" | "invite">(initialTab); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -64,6 +76,12 @@ export function ProjectCreateMenuModal({ onClose, initialTab = "repo" }: Project }); const createSkill = useCreateSkillMutation(projectName); + // --- Invite Form State --- + const [inviteForm, setInviteForm] = useState({ + email: "", + scope: "Member" as MemberRole, + }); + const handleCreateRepo = async (e: React.FormEvent) => { e.preventDefault(); if (!repoForm.repo_name.trim()) return; @@ -149,6 +167,25 @@ export function ProjectCreateMenuModal({ onClose, initialTab = "repo" }: Project } }; + const handleInviteUser = async (e: React.FormEvent) => { + e.preventDefault(); + if (!projectName || !inviteForm.email.trim()) return; + try { + setLoading(true); + setError(null); + await projectInviteUser(projectName, { + email: inviteForm.email.trim(), + scope: inviteForm.scope, + }); + setInviteForm({ email: "", scope: "Member" }); + onClose(); + } catch (err: unknown) { + setError((err as { response?: { data?: { message?: string } } })?.response?.data?.message || "Failed to send invitation."); + } finally { + setLoading(false); + } + }; + return (
- Quick Create + Quick Start +
+ + )}
); -} \ No newline at end of file +} diff --git a/src/app/project/settings/MembersSettings.tsx b/src/app/project/settings/MembersSettings.tsx index f0191bc..a348b5a 100644 --- a/src/app/project/settings/MembersSettings.tsx +++ b/src/app/project/settings/MembersSettings.tsx @@ -7,6 +7,7 @@ import { useProjectInfo } from "@/hooks/useProjectInfo"; import { Button } from "@/components/ui/button"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Loader2 } from "lucide-react"; +import { isProjectAdminRole, isProjectOwnerRole } from "@/lib/project-permissions"; const NEXT_ROLE: Record = { admin: "member", member: "admin" }; @@ -14,8 +15,8 @@ export function MembersSettings() { const { projectName } = useParams<{ projectName: string }>(); const { data: projectInfo } = useProjectInfo(projectName); const queryClient = useQueryClient(); - const isCurrentAdmin = projectInfo?.role === "Owner" || projectInfo?.role === "Admin"; - const isCurrentOwner = projectInfo?.role === "Owner"; + const isCurrentAdmin = isProjectAdminRole(projectInfo?.role); + const isCurrentOwner = isProjectOwnerRole(projectInfo?.role); const { data, isLoading, error } = useQuery({ queryKey: ["project-members-grouped", projectName], @@ -64,7 +65,7 @@ export function MembersSettings() {
{group.members.map(member => { const isSelf = member.user_id === projectInfo?.created_by; - const canManage = isCurrentAdmin && member.scope !== "Owner" && !isSelf; + const canManage = isCurrentAdmin && !isProjectOwnerRole(member.scope) && !isSelf; return (
diff --git a/src/app/project/settings/ProjectSettingsLayout.tsx b/src/app/project/settings/ProjectSettingsLayout.tsx index e95001b..33cc8a1 100644 --- a/src/app/project/settings/ProjectSettingsLayout.tsx +++ b/src/app/project/settings/ProjectSettingsLayout.tsx @@ -1,12 +1,13 @@ import { Outlet, useParams, NavLink } from "react-router-dom"; import { useProjectInfo } from "@/hooks/useProjectInfo"; import { Loader2, Lock, Settings, Users, Key, Tag, CreditCard } from "lucide-react"; +import { isProjectAdminRole } from "@/lib/project-permissions"; export function ProjectSettingsLayout() { const { projectName } = useParams<{ projectName: string }>(); const { data: info, isLoading, error } = useProjectInfo(projectName); - const isAdmin = info?.role === "Owner" || info?.role === "Admin"; + const isAdmin = isProjectAdminRole(info?.role); const tabs = [ { to: "", end: true, icon: Settings, label: "General" },