|
| 1 | +import { FunctionComponent, useMemo, useState } from "react"; |
| 2 | +import { |
| 3 | + Dialog, |
| 4 | + DialogContent, |
| 5 | + DialogDescription, |
| 6 | + DialogFooter, |
| 7 | + DialogHeader, |
| 8 | + DialogTitle, |
| 9 | +} from "./ui/dialog"; |
| 10 | + |
| 11 | +import { useActiveOrg } from "@/hooks/useActiveOrg"; |
| 12 | +import { browserApiClient } from "@/services/devGuardApi"; |
| 13 | +import { toast } from "sonner"; |
| 14 | +import { useActiveProject } from "../hooks/useActiveProject"; |
| 15 | +import Callout from "./common/Callout"; |
| 16 | +import { |
| 17 | + DropdownMenu, |
| 18 | + DropdownMenuCheckboxItem, |
| 19 | + DropdownMenuContent, |
| 20 | + DropdownMenuTrigger, |
| 21 | +} from "./ui/dropdown-menu"; |
| 22 | +import { cn } from "../lib/utils"; |
| 23 | +import { Button } from "./ui/button"; |
| 24 | +import { useStore } from "../zustand/globalStoreProvider"; |
| 25 | + |
| 26 | +interface Props { |
| 27 | + isOpen: boolean; |
| 28 | + onOpenChange: (open: boolean) => void; |
| 29 | +} |
| 30 | + |
| 31 | +const ProjectMemberDialog: FunctionComponent<Props> = ({ |
| 32 | + isOpen, |
| 33 | + onOpenChange, |
| 34 | +}) => { |
| 35 | + const activeOrg = useActiveOrg(); |
| 36 | + const activeProject = useActiveProject(); |
| 37 | + const updateProject = useStore((s) => s.updateProject); |
| 38 | + const [selectedMembers, setSelectedMembers] = useState<string[]>([]); |
| 39 | + |
| 40 | + const handleInviteSelectedMembers = async () => { |
| 41 | + const resp = await browserApiClient( |
| 42 | + "/organizations/" + |
| 43 | + activeOrg.slug + |
| 44 | + "/projects/" + |
| 45 | + activeProject.slug + |
| 46 | + "/members", |
| 47 | + { |
| 48 | + method: "POST", |
| 49 | + body: JSON.stringify({ |
| 50 | + ids: selectedMembers, |
| 51 | + }), |
| 52 | + }, |
| 53 | + ); |
| 54 | + |
| 55 | + if (!resp.ok) { |
| 56 | + toast.error("Failed to invite member"); |
| 57 | + return; |
| 58 | + } else { |
| 59 | + updateProject({ |
| 60 | + ...activeProject, |
| 61 | + members: activeProject.members.concat( |
| 62 | + activeOrg.members |
| 63 | + .filter((e) => selectedMembers.includes(e.id)) |
| 64 | + .map((e) => ({ |
| 65 | + ...e, |
| 66 | + role: "member", |
| 67 | + })), |
| 68 | + ), |
| 69 | + }); |
| 70 | + toast.success("Members successfully added to the project"); |
| 71 | + onOpenChange(false); |
| 72 | + } |
| 73 | + }; |
| 74 | + |
| 75 | + const membersToInvite = useMemo(() => { |
| 76 | + const projectMemberIds = activeProject.members.reduce( |
| 77 | + (acc, m) => { |
| 78 | + acc[m.id] = true; |
| 79 | + return acc; |
| 80 | + }, |
| 81 | + {} as { [key: string]: boolean }, |
| 82 | + ); |
| 83 | + return activeOrg.members.filter((m) => !projectMemberIds[m.id]); |
| 84 | + }, [activeProject.members, activeOrg.members]); |
| 85 | + |
| 86 | + return ( |
| 87 | + <Dialog open={isOpen} onOpenChange={onOpenChange}> |
| 88 | + <DialogContent> |
| 89 | + <DialogHeader> |
| 90 | + <DialogTitle>Invite member</DialogTitle> |
| 91 | + <DialogDescription> |
| 92 | + Invite a new member to your organization by entering their email |
| 93 | + address. |
| 94 | + </DialogDescription> |
| 95 | + </DialogHeader> |
| 96 | + <DropdownMenu> |
| 97 | + <DropdownMenuTrigger |
| 98 | + className={cn( |
| 99 | + "flex h-10 w-full items-center justify-between rounded-md border border-input bg-card px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", |
| 100 | + )} |
| 101 | + > |
| 102 | + Select the people you would like to invite to the project. ( |
| 103 | + {selectedMembers.length} currently selected) |
| 104 | + </DropdownMenuTrigger> |
| 105 | + <DropdownMenuContent className="w-full"> |
| 106 | + {membersToInvite.map((m) => ( |
| 107 | + <DropdownMenuCheckboxItem |
| 108 | + onClick={() => |
| 109 | + setSelectedMembers((prev) => { |
| 110 | + if (prev.includes(m.id)) { |
| 111 | + return prev.filter((el) => el !== m.id); |
| 112 | + } |
| 113 | + return prev.concat(m.id); |
| 114 | + }) |
| 115 | + } |
| 116 | + checked={selectedMembers.includes(m.id)} |
| 117 | + key={m.id} |
| 118 | + > |
| 119 | + {m.name} |
| 120 | + </DropdownMenuCheckboxItem> |
| 121 | + ))} |
| 122 | + </DropdownMenuContent> |
| 123 | + </DropdownMenu> |
| 124 | + <DialogFooter className="mt-2"> |
| 125 | + <div className="flex flex-col items-end justify-end gap-2"> |
| 126 | + <Button onClick={handleInviteSelectedMembers} type="submit"> |
| 127 | + Invite |
| 128 | + </Button> |
| 129 | + </div> |
| 130 | + </DialogFooter> |
| 131 | + </DialogContent> |
| 132 | + </Dialog> |
| 133 | + ); |
| 134 | +}; |
| 135 | + |
| 136 | +export default ProjectMemberDialog; |
0 commit comments