Allow user to change projects

This commit is contained in:
David Corbitt
2023-08-07 15:18:23 -07:00
parent c0f10cd522
commit d220cd30e8
3 changed files with 172 additions and 30 deletions

View File

@@ -3,8 +3,9 @@ import { useRouter } from "next/router";
const NavSidebarOption = ({ const NavSidebarOption = ({
activeHrefPattern, activeHrefPattern,
disableHoverEffect,
...props ...props
}: { activeHrefPattern?: string } & BoxProps) => { }: { activeHrefPattern?: string; disableHoverEffect?: boolean } & BoxProps) => {
const router = useRouter(); const router = useRouter();
const isActive = activeHrefPattern && router.pathname.startsWith(activeHrefPattern); const isActive = activeHrefPattern && router.pathname.startsWith(activeHrefPattern);
return ( return (
@@ -12,7 +13,7 @@ const NavSidebarOption = ({
w="full" w="full"
fontWeight={isActive ? "bold" : "500"} fontWeight={isActive ? "bold" : "500"}
bgColor={isActive ? "gray.200" : "transparent"} bgColor={isActive ? "gray.200" : "transparent"}
_hover={{ bgColor: "gray.200", textDecoration: "none" }} _hover={disableHoverEffect ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
justifyContent="start" justifyContent="start"
cursor="pointer" cursor="pointer"
borderRadius={4} borderRadius={4}

View File

@@ -6,33 +6,63 @@ import {
PopoverTrigger, PopoverTrigger,
PopoverContent, PopoverContent,
Flex, Flex,
IconButton,
Icon,
Divider,
Button,
useDisclosure,
Spinner,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { AiFillCaretDown } from "react-icons/ai";
import { BsGear, BsPlus } from "react-icons/bs";
import { type Organization } from "@prisma/client";
import { useAppStore } from "~/state/store"; import { useAppStore } from "~/state/store";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import NavSidebarOption from "./NavSidebarOption"; import NavSidebarOption from "./NavSidebarOption";
import { useSelectedOrg } from "~/utils/hooks"; import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks";
import { useRouter } from "next/router";
export default function ProjectMenu() { export default function ProjectMenu() {
const router = useRouter();
const isActive = router.pathname.startsWith("/home");
const utils = api.useContext();
const selectedOrgId = useAppStore((s) => s.selectedOrgId); const selectedOrgId = useAppStore((s) => s.selectedOrgId);
const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId); const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId);
const { data } = api.organizations.list.useQuery(); const { data: orgs } = api.organizations.list.useQuery();
useEffect(() => { useEffect(() => {
if (data && data[0] && (!selectedOrgId || !data.find((org) => org.id === selectedOrgId))) { if (orgs && orgs[0] && (!selectedOrgId || !orgs.find((org) => org.id === selectedOrgId))) {
setSelectedOrgId(data[0].id); setSelectedOrgId(orgs[0].id);
} }
}, [selectedOrgId, setSelectedOrgId, data]); }, [selectedOrgId, setSelectedOrgId, orgs]);
const { data: selectedOrg } = useSelectedOrg(); const { data: selectedOrg } = useSelectedOrg();
const [expandButtonHovered, setExpandButtonHovered] = useState(false);
const popover = useDisclosure();
const createMutation = api.organizations.create.useMutation();
const [createProject, isLoading] = useHandledAsyncCallback(async () => {
const newOrg = await createMutation.mutateAsync({ name: "New Project" });
await utils.organizations.list.invalidate();
setSelectedOrgId(newOrg.id);
await router.push({ pathname: "/settings" });
}, [createMutation, router]);
return ( return (
<> <>
<Popover placement="right"> <Popover
<PopoverTrigger> placement="bottom-start"
isOpen={popover.isOpen}
onClose={popover.onClose}
closeOnBlur
>
<VStack w="full" alignItems="flex-start" spacing={0}> <VStack w="full" alignItems="flex-start" spacing={0}>
<Text <Text
pl={2} pl={2}
@@ -44,9 +74,10 @@ export default function ProjectMenu() {
> >
PROJECT PROJECT
</Text> </Text>
<NavSidebarOption activeHrefPattern="/home"> <NavSidebarOption activeHrefPattern="/home" disableHoverEffect={expandButtonHovered}>
<Link href="/home"> <Link href="/home">
<HStack w="full"> <HStack w="full" justifyContent="space-between">
<HStack>
<Flex <Flex
p={1} p={1}
borderRadius={4} borderRadius={4}
@@ -63,12 +94,93 @@ export default function ProjectMenu() {
{selectedOrg?.name} {selectedOrg?.name}
</Text> </Text>
</HStack> </HStack>
<PopoverTrigger>
<IconButton
aria-label="Open Project Menu"
icon={<Icon as={AiFillCaretDown} boxSize={3} />}
size="xs"
colorScheme="gray"
color="gray.500"
variant="ghost"
mr={2}
borderRadius={4}
onMouseEnter={() => setExpandButtonHovered(true)}
onMouseLeave={() => setExpandButtonHovered(false)}
_hover={{ bgColor: isActive ? "gray.300" : "gray.200", transitionDelay: 0 }}
onClick={(event) => {
event.preventDefault();
popover.onToggle();
}}
/>
</PopoverTrigger>
</HStack>
</Link> </Link>
</NavSidebarOption> </NavSidebarOption>
</VStack> </VStack>
</PopoverTrigger> <PopoverContent
<PopoverContent _focusVisible={{ boxShadow: "unset", outline: "unset" }}></PopoverContent> _focusVisible={{ boxShadow: "unset" }}
minW={0}
borderColor="blue.400"
w="auto"
>
<VStack alignItems="flex-start" spacing={2} py={4} px={2}>
<Text color="gray.500" fontSize="xs" fontWeight="bold" pb={1}>
PROJECTS
</Text>
<Divider />
<VStack spacing={0}>
{orgs?.map((org) => (
<ProjectOption key={org.id} org={org} isActive={org.id === selectedOrgId} />
))}
</VStack>
<HStack
as={Button}
variant="ghost"
colorScheme="blue"
color="blue.400"
pr={8}
w="full"
onClick={createProject}
>
<Icon as={isLoading ? Spinner : BsPlus} boxSize={6} />
<Text>New project</Text>
</HStack>
</VStack>
</PopoverContent>
</Popover> </Popover>
</> </>
); );
} }
const ProjectOption = ({ org, isActive }: { org: Organization; isActive: boolean }) => {
const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId);
const [gearHovered, setGearHovered] = useState(false);
return (
<HStack
as={Link}
href="/home"
onClick={() => setSelectedOrgId(org.id)}
w="full"
justifyContent="space-between"
bgColor={isActive ? "gray.100" : "transparent"}
_hover={gearHovered ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
p={2}
borderRadius={4}
>
<Text>{org.name}</Text>
<IconButton
as={Link}
href="/settings"
aria-label={`Open ${org.name} settings`}
icon={<Icon as={BsGear} boxSize={5} strokeWidth={0.5} color="gray.500" />}
variant="ghost"
size="xs"
p={0}
onMouseEnter={() => setGearHovered(true)}
onMouseLeave={() => setGearHovered(false)}
_hover={{ bgColor: isActive ? "gray.300" : "gray.100", transitionDelay: 0 }}
borderRadius={4}
/>
</HStack>
);
};

View File

@@ -22,7 +22,7 @@ export const organizationsRouter = createTRPCRouter({
}, },
}, },
orderBy: { orderBy: {
createdAt: "desc", createdAt: "asc",
}, },
}); });
@@ -63,7 +63,7 @@ export const organizationsRouter = createTRPCRouter({
}, },
include: { include: {
apiKeys: true, apiKeys: true,
} },
}); });
}), }),
update: protectedProcedure update: protectedProcedure
@@ -79,4 +79,33 @@ export const organizationsRouter = createTRPCRouter({
}, },
}); });
}), }),
create: protectedProcedure
.input(z.object({ name: z.string() }))
.mutation(async ({ input, ctx }) => {
requireNothing(ctx);
const newOrgId = uuidv4();
const [newOrg] = await prisma.$transaction([
prisma.organization.create({
data: {
id: newOrgId,
name: input.name,
},
}),
prisma.organizationUser.create({
data: {
userId: ctx.session.user.id,
organizationId: newOrgId,
role: "ADMIN",
},
}),
prisma.apiKey.create({
data: {
name: "Default API Key",
organizationId: newOrgId,
apiKey: generateApiKey(),
},
}),
]);
return newOrg;
}),
}); });