From 70b9d435d1176216cf2ab177c23fd38283e5fc07 Mon Sep 17 00:00:00 2001 From: Ronit Panda Date: Fri, 13 Oct 2023 10:42:54 +0530 Subject: [PATCH 1/4] feat: adds ability to create multiple orgs under the same account --- frontend/src/layouts/AppLayout/AppLayout.tsx | 68 +++++++--- frontend/src/views/Org/NonePage/NonePage.tsx | 121 +++--------------- .../views/Org/components/CreateOrgModal.tsx | 94 ++++++++++++++ frontend/src/views/Org/components/index.tsx | 1 + 4 files changed, 168 insertions(+), 116 deletions(-) create mode 100644 frontend/src/views/Org/components/CreateOrgModal.tsx create mode 100644 frontend/src/views/Org/components/index.tsx diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 4f0cba998..7200f92c9 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -71,7 +71,9 @@ import { useGetUserAction, useLogoutUser, useRegisterUserAction, - useUploadWsKey} from "@app/hooks/api"; + useUploadWsKey +} from "@app/hooks/api"; +import { CreateOrgModal } from "@app/views/Org/components"; interface LayoutProps { children: React.ReactNode; @@ -115,12 +117,12 @@ export const AppLayout = ({ children }: LayoutProps) => { // eslint-disable-next-line prefer-const const { workspaces, currentWorkspace } = useWorkspace(); const { orgs, currentOrg } = useOrganization(); - + const { user } = useUser(); const { subscription } = useSubscription(); const workspaceId = currentWorkspace?._id || ""; const { data: updateClosed } = useGetUserAction("september_update_closed"); - + const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId }); const isAddingProjectsAllowed = subscription?.workspaceLimit @@ -134,7 +136,8 @@ export const AppLayout = ({ children }: LayoutProps) => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "addNewWs", - "upgradePlan" + "upgradePlan", + "createOrg" ] as const); const { control, @@ -151,7 +154,7 @@ export const AppLayout = ({ children }: LayoutProps) => { const closeUpdate = async () => { await registerUserAction.mutateAsync("september_update_closed"); - } + }; const logout = useLogoutUser(); const logOutUser = async () => { @@ -318,6 +321,22 @@ export const AppLayout = ({ children }: LayoutProps) => { ))} + + +
@@ -619,9 +652,10 @@ export const AppLayout = ({ children }: LayoutProps) => { href="https://infisical.com/blog/infisical-update-september-2023" target="_blank" rel="noopener noreferrer" - className="text-sm text-mineshaft-400 font-normal leading-[1.2rem] hover:text-mineshaft-100 duration-200" + className="text-sm font-normal leading-[1.2rem] text-mineshaft-400 duration-200 hover:text-mineshaft-100" > - Learn More + Learn More{" "} +
@@ -780,6 +814,10 @@ export const AppLayout = ({ children }: LayoutProps) => { onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} text="You have exceeded the number of projects allowed on the free plan." /> + handlePopUpToggle("createOrg", false)} + />
{children}
diff --git a/frontend/src/views/Org/NonePage/NonePage.tsx b/frontend/src/views/Org/NonePage/NonePage.tsx index ec7a768a3..26294a36b 100644 --- a/frontend/src/views/Org/NonePage/NonePage.tsx +++ b/frontend/src/views/Org/NonePage/NonePage.tsx @@ -1,112 +1,31 @@ import { useEffect } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { - Button, - FormControl, - Input, - Modal, - ModalContent} from "@app/components/v2"; -import { useCreateOrg } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; -const schema = yup.object({ - name: yup.string().required("Organization name is required"), -}).required(); +import { CreateOrgModal } from "../components"; + +const schema = yup + .object({ + name: yup.string().required("Organization name is required") + }) + .required(); export type FormData = yup.InferType; export const NonePage = () => { - const { createNotification } = useNotificationContext(); - const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ - "createOrg", - ] as const); - - const { mutateAsync } = useCreateOrg(); - - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ - resolver: yupResolver(schema), - defaultValues: { - name: "" - } - }); - - useEffect(() => { - handlePopUpOpen("createOrg"); - }, []); - - const onFormSubmit = async ({ name }: FormData) => { - try { - - const organization = await mutateAsync({ - name - }); + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["createOrg"] as const); - createNotification({ - text: "Successfully created organization", - type: "success" - }); + useEffect(() => { + handlePopUpOpen("createOrg"); + }, []); - window.location.href = `/org/${organization._id}/overview`; - - reset(); - handlePopUpToggle("createOrg", false); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to created organization", - type: "error" - }); - } - } - - return ( -
- - -
- ( - - - - )} - /> - - -
-
-
- ); -} \ No newline at end of file + return ( +
+ handlePopUpToggle("createOrg", false)} + /> +
+ ); +}; diff --git a/frontend/src/views/Org/components/CreateOrgModal.tsx b/frontend/src/views/Org/components/CreateOrgModal.tsx new file mode 100644 index 000000000..ccf1c4d31 --- /dev/null +++ b/frontend/src/views/Org/components/CreateOrgModal.tsx @@ -0,0 +1,94 @@ +import { FC } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; +import { useCreateOrg } from "@app/hooks/api"; + +const schema = yup + .object({ + name: yup.string().required("Organization name is required") + }) + .required(); + +export type FormData = yup.InferType; + +interface CreateOrgModalProps { + isOpen: boolean; + onClose: () => void; +} + +export const CreateOrgModal: FC = ({ isOpen, onClose }) => { + const { createNotification } = useNotificationContext(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + name: "" + } + }); + + const { mutateAsync } = useCreateOrg(); + + const onFormSubmit = async ({ name }: FormData) => { + try { + const organization = await mutateAsync({ + name + }); + + createNotification({ + text: "Successfully created organization", + type: "success" + }); + + window.location.href = `/org/${organization._id}/overview`; + + reset(); + onClose(); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to created organization", + type: "error" + }); + } + }; + + return ( + + +
+ ( + + + + )} + /> + + +
+
+ ); +}; diff --git a/frontend/src/views/Org/components/index.tsx b/frontend/src/views/Org/components/index.tsx new file mode 100644 index 000000000..7b794731a --- /dev/null +++ b/frontend/src/views/Org/components/index.tsx @@ -0,0 +1 @@ +export { CreateOrgModal } from "./CreateOrgModal"; From 840eef7bce6c8cf5fc263461f22ef0c271f9cfdb Mon Sep 17 00:00:00 2001 From: Ronit Panda Date: Fri, 13 Oct 2023 11:05:24 +0530 Subject: [PATCH 2/4] feat: improves the flow of account creation --- .../src/hooks/api/organization/queries.tsx | 127 +++++++++--------- .../views/Org/components/CreateOrgModal.tsx | 7 +- 2 files changed, 68 insertions(+), 66 deletions(-) diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 697daede5..1b075249c 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -2,11 +2,11 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { +import { BillingDetails, Invoice, License, - Organization, + Organization, OrgPlanTable, PlanBillingInfo, PmtMethod, @@ -19,7 +19,8 @@ const organizationKeys = { getUserOrganizations: ["organization"] as const, getOrgPlanBillingInfo: (orgId: string) => [{ orgId }, "organization-plan-billing"] as const, getOrgPlanTable: (orgId: string) => [{ orgId }, "organization-plan-table"] as const, - getOrgPlansTable: (orgId: string, billingCycle: "monthly" | "yearly") => [{ orgId, billingCycle }, "organization-plans-table"] as const, + getOrgPlansTable: (orgId: string, billingCycle: "monthly" | "yearly") => + [{ orgId, billingCycle }, "organization-plans-table"] as const, getOrgBillingDetails: (orgId: string) => [{ orgId }, "organization-billing-details"] as const, getOrgPmtMethods: (orgId: string) => [{ orgId }, "organization-pmt-methods"] as const, getOrgTaxIds: (orgId: string) => [{ orgId }, "organization-tax-ids"] as const, @@ -28,34 +29,36 @@ const organizationKeys = { }; export const fetchOrganizations = async () => { - const { data: { organizations } } = await apiRequest.get<{ organizations: Organization[] }>("/api/v1/organization"); + const { + data: { organizations } + } = await apiRequest.get<{ organizations: Organization[] }>("/api/v1/organization"); return organizations; -} +}; export const useGetOrganizations = () => { - return useQuery({ - queryKey: organizationKeys.getUserOrganizations, + return useQuery({ + queryKey: organizationKeys.getUserOrganizations, queryFn: async () => { return fetchOrganizations(); } }); -} +}; export const useCreateOrg = () => { + const queryClient = useQueryClient(); + return useMutation({ - mutationFn: async ({ - name - }: { - name: string; - }) => { - const { data: { organization } } = await apiRequest.post( - "/api/v2/organizations", - { - name - } - ); + mutationFn: async ({ name }: { name: string }) => { + const { + data: { organization } + } = await apiRequest.post("/api/v2/organizations", { + name + }); return organization; + }, + onSuccess: () => { + queryClient.invalidateQueries(organizationKeys.getUserOrganizations); } }); }; @@ -75,17 +78,13 @@ export const useRenameOrg = () => { export const useGetOrgTrialUrl = () => { return useMutation({ - mutationFn: async ({ - orgId, - success_url - }: { - orgId: string; - success_url: string; - }) => { - const { data: { url } } = await apiRequest.post(`/api/v1/organizations/${orgId}/session/trial`, { + mutationFn: async ({ orgId, success_url }: { orgId: string; success_url: string }) => { + const { + data: { url } + } = await apiRequest.post(`/api/v1/organizations/${orgId}/session/trial`, { success_url - }) - + }); + return url; } }); @@ -99,11 +98,11 @@ export const useGetOrgPlanBillingInfo = (organizationId: string) => { `/api/v1/organizations/${organizationId}/plan/billing` ); - return data; + return data; }, enabled: true }); -} +}; export const useGetOrgPlanTable = (organizationId: string) => { return useQuery({ @@ -113,18 +112,18 @@ export const useGetOrgPlanTable = (organizationId: string) => { `/api/v1/organizations/${organizationId}/plan/table` ); - return data; + return data; }, enabled: true }); -} +}; export const useGetOrgPlansTable = ({ organizationId, billingCycle }: { organizationId: string; - billingCycle: "monthly" | "yearly" + billingCycle: "monthly" | "yearly"; }) => { return useQuery({ queryKey: organizationKeys.getOrgPlansTable(organizationId, billingCycle), @@ -133,11 +132,11 @@ export const useGetOrgPlansTable = ({ `/api/v1/organizations/${organizationId}/plans/table?billingCycle=${billingCycle}` ); - return data; + return data; }, enabled: true }); -} +}; export const useGetOrgBillingDetails = (organizationId: string) => { return useQuery({ @@ -151,7 +150,7 @@ export const useGetOrgBillingDetails = (organizationId: string) => { }, enabled: true }); -} +}; export const useUpdateOrgBillingDetails = () => { const queryClient = useQueryClient(); @@ -166,7 +165,7 @@ export const useUpdateOrgBillingDetails = () => { email?: string; }) => { const { data } = await apiRequest.patch( - `/api/v1/organizations/${organizationId}/billing-details`, + `/api/v1/organizations/${organizationId}/billing-details`, { name, email @@ -193,7 +192,7 @@ export const useGetOrgPmtMethods = (organizationId: string) => { }, enabled: true }); -} +}; export const useAddOrgPmtMethod = () => { const queryClient = useQueryClient(); @@ -208,8 +207,10 @@ export const useAddOrgPmtMethod = () => { success_url: string; cancel_url: string; }) => { - const { data: { url } } = await apiRequest.post( - `/api/v1/organizations/${organizationId}/billing-details/payment-methods`, + const { + data: { url } + } = await apiRequest.post( + `/api/v1/organizations/${organizationId}/billing-details/payment-methods`, { success_url, cancel_url @@ -230,7 +231,7 @@ export const useDeleteOrgPmtMethod = () => { return useMutation({ mutationFn: async ({ organizationId, - pmtMethodId, + pmtMethodId }: { organizationId: string; pmtMethodId: string; @@ -245,7 +246,7 @@ export const useDeleteOrgPmtMethod = () => { queryClient.invalidateQueries(organizationKeys.getOrgPmtMethods(dto.organizationId)); } }); -} +}; export const useGetOrgTaxIds = (organizationId: string) => { return useQuery({ @@ -259,7 +260,7 @@ export const useGetOrgTaxIds = (organizationId: string) => { }, enabled: true }); -} +}; export const useAddOrgTaxId = () => { const queryClient = useQueryClient(); @@ -275,7 +276,7 @@ export const useAddOrgTaxId = () => { value: string; }) => { const { data } = await apiRequest.post( - `/api/v1/organizations/${organizationId}/billing-details/tax-ids`, + `/api/v1/organizations/${organizationId}/billing-details/tax-ids`, { type, value @@ -294,13 +295,7 @@ export const useDeleteOrgTaxId = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ - organizationId, - taxId, - }: { - organizationId: string; - taxId: string; - }) => { + mutationFn: async ({ organizationId, taxId }: { organizationId: string; taxId: string }) => { const { data } = await apiRequest.delete( `/api/v1/organizations/${organizationId}/billing-details/tax-ids/${taxId}` ); @@ -311,7 +306,7 @@ export const useDeleteOrgTaxId = () => { queryClient.invalidateQueries(organizationKeys.getOrgTaxIds(dto.organizationId)); } }); -} +}; export const useGetOrgInvoices = (organizationId: string) => { return useQuery({ @@ -325,7 +320,7 @@ export const useGetOrgInvoices = (organizationId: string) => { }, enabled: true }); -} +}; export const useCreateCustomerPortalSession = () => { return useMutation({ @@ -343,7 +338,7 @@ export const useGetOrgLicenses = (organizationId: string) => { queryKey: organizationKeys.getOrgLicenses(organizationId), queryFn: async () => { if (organizationId === "") return undefined; - + const { data } = await apiRequest.get( `/api/v1/organizations/${organizationId}/licenses` ); @@ -352,18 +347,16 @@ export const useGetOrgLicenses = (organizationId: string) => { }, enabled: true }); -} +}; export const useDeleteOrgById = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ - organizationId, - }: { - organizationId: string; - }) => { - const { data: { organization } } = await apiRequest.delete<{ organization: Organization }>( + mutationFn: async ({ organizationId }: { organizationId: string }) => { + const { + data: { organization } + } = await apiRequest.delete<{ organization: Organization }>( `/api/v2/organizations/${organizationId}` ); return organization; @@ -372,8 +365,12 @@ export const useDeleteOrgById = () => { queryClient.invalidateQueries(organizationKeys.getUserOrganizations); queryClient.invalidateQueries(organizationKeys.getOrgPlanBillingInfo(dto.organizationId)); queryClient.invalidateQueries(organizationKeys.getOrgPlanTable(dto.organizationId)); - queryClient.invalidateQueries(organizationKeys.getOrgPlansTable(dto.organizationId, "monthly")); // You might need to invalidate for 'yearly' as well. - queryClient.invalidateQueries(organizationKeys.getOrgPlansTable(dto.organizationId, "yearly")); + queryClient.invalidateQueries( + organizationKeys.getOrgPlansTable(dto.organizationId, "monthly") + ); // You might need to invalidate for 'yearly' as well. + queryClient.invalidateQueries( + organizationKeys.getOrgPlansTable(dto.organizationId, "yearly") + ); queryClient.invalidateQueries(organizationKeys.getOrgBillingDetails(dto.organizationId)); queryClient.invalidateQueries(organizationKeys.getOrgPmtMethods(dto.organizationId)); queryClient.invalidateQueries(organizationKeys.getOrgTaxIds(dto.organizationId)); @@ -381,4 +378,4 @@ export const useDeleteOrgById = () => { queryClient.invalidateQueries(organizationKeys.getOrgLicenses(dto.organizationId)); } }); -} \ No newline at end of file +}; diff --git a/frontend/src/views/Org/components/CreateOrgModal.tsx b/frontend/src/views/Org/components/CreateOrgModal.tsx index ccf1c4d31..273755929 100644 --- a/frontend/src/views/Org/components/CreateOrgModal.tsx +++ b/frontend/src/views/Org/components/CreateOrgModal.tsx @@ -1,5 +1,6 @@ import { FC } from "react"; import { Controller, useForm } from "react-hook-form"; +import { useRouter } from "next/router"; import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; @@ -22,6 +23,7 @@ interface CreateOrgModalProps { export const CreateOrgModal: FC = ({ isOpen, onClose }) => { const { createNotification } = useNotificationContext(); + const router = useRouter(); const { control, @@ -48,7 +50,10 @@ export const CreateOrgModal: FC = ({ isOpen, onClose }) => type: "success" }); - window.location.href = `/org/${organization._id}/overview`; + if (router.isReady) router.push(`/org/${organization._id}/overview`); + else window.location.href = `/org/${organization._id}/overview`; + + localStorage.setItem("orgData.id", organization._id); reset(); onClose(); From 1476d06b7e07f32c4ff377b05ff93d163930226a Mon Sep 17 00:00:00 2001 From: Ronit Panda Date: Tue, 24 Oct 2023 06:02:20 +0530 Subject: [PATCH 3/4] feat: adds cancel button and uses zod over yup --- .../views/Org/components/CreateOrgModal.tsx | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/frontend/src/views/Org/components/CreateOrgModal.tsx b/frontend/src/views/Org/components/CreateOrgModal.tsx index 273755929..b62dfb9cb 100644 --- a/frontend/src/views/Org/components/CreateOrgModal.tsx +++ b/frontend/src/views/Org/components/CreateOrgModal.tsx @@ -1,20 +1,20 @@ import { FC } from "react"; import { Controller, useForm } from "react-hook-form"; import { useRouter } from "next/router"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; +import { zodResolver } from "@hookform/resolvers/zod"; +import z from "zod"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; import { useCreateOrg } from "@app/hooks/api"; -const schema = yup +const schema = z .object({ - name: yup.string().required("Organization name is required") + name: z.string().nonempty({ message: "Name is required" }) }) .required(); -export type FormData = yup.InferType; +export type FormData = z.infer; interface CreateOrgModalProps { isOpen: boolean; @@ -31,7 +31,7 @@ export const CreateOrgModal: FC = ({ isOpen, onClose }) => reset, formState: { isSubmitting } } = useForm({ - resolver: yupResolver(schema), + resolver: zodResolver(schema), defaultValues: { name: "" } @@ -83,15 +83,20 @@ export const CreateOrgModal: FC = ({ isOpen, onClose }) => )} /> - +
+ + +
From 619bbf2027a73eb59151831f43f27ca777646b95 Mon Sep 17 00:00:00 2001 From: Ronit Panda Date: Tue, 24 Oct 2023 06:09:12 +0530 Subject: [PATCH 4/4] fix: fixes broken nonePage.tsx --- frontend/src/views/Org/NonePage/NonePage.tsx | 52 +------------------- 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/frontend/src/views/Org/NonePage/NonePage.tsx b/frontend/src/views/Org/NonePage/NonePage.tsx index e2fd70fd2..9d6b4a65c 100644 --- a/frontend/src/views/Org/NonePage/NonePage.tsx +++ b/frontend/src/views/Org/NonePage/NonePage.tsx @@ -1,59 +1,9 @@ -import { useEffect } from "react"; -import * as yup from "yup"; - import { usePopUp } from "@app/hooks/usePopUp"; import { CreateOrgModal } from "../components"; -const schema = yup - .object({ - name: yup.string().required("Organization name is required") - }) - .required(); - -export type FormData = yup.InferType; - export const NonePage = () => { - const { createNotification } = useNotificationContext(); - const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ - "createOrg", - ] as const); - - const { mutateAsync } = useCreateOrg(); - - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ - resolver: yupResolver(schema), - defaultValues: { - name: "" - } - }); - - useEffect(() => { - handlePopUpOpen("createOrg"); - }, []); - - const onFormSubmit = async ({ name }: FormData) => { - try { - - const organization = await mutateAsync({ - name - }); - - localStorage.setItem("orgData.id", organization._id); - - createNotification({ - text: "Successfully created organization", - type: "success" - }); - - useEffect(() => { - handlePopUpOpen("createOrg"); - }, []); + const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const); return (