fix: new sub-org creation + sub-org nav styling

This commit is contained in:
Piyush Gupta
2025-11-28 19:06:40 +05:30
parent 25240789f6
commit 23c4a695f6
5 changed files with 160 additions and 93 deletions
@@ -198,10 +198,12 @@ export const Navbar = () => {
const handleOrgSelection = async ({ const handleOrgSelection = async ({
organizationId, organizationId,
subOrganizationId, subOrganizationId,
navigateTo,
onSuccess onSuccess
}: { }: {
organizationId?: string; organizationId?: string;
subOrganizationId?: string; subOrganizationId?: string;
navigateTo?: string;
onSuccess?: () => void | Promise<void>; onSuccess?: () => void | Promise<void>;
}) => { }) => {
if (!organizationId && !subOrganizationId) return; if (!organizationId && !subOrganizationId) return;
@@ -234,7 +236,7 @@ export const Navbar = () => {
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() }); queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
await router.invalidate(); await router.invalidate();
await navigateUserToOrg(navigate, targetId); await navigateUserToOrg({ navigate, organizationId: targetId, navigateTo });
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey }); queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
if (onSuccess) { if (onSuccess) {
@@ -243,21 +245,38 @@ export const Navbar = () => {
}; };
const handleNavigateToRootOrgBilling = async () => { const handleNavigateToRootOrgBilling = async () => {
const navigateToBilling = async () => { const navigateToBilling = () => {
navigate({ navigate({
to: "/organizations/$orgId/billing", to: "/organizations/$orgId/billing",
params: { orgId: rootOrg.id } params: { orgId: rootOrg.id }
}); });
};
const onSuccess = () => {
setShowCardDeclinedModal(false); setShowCardDeclinedModal(false);
}; };
if (currentOrg.id !== rootOrg.id) { if (isSubOrganization) {
await handleOrgSelection({ organizationId: rootOrg.id, onSuccess: navigateToBilling }); await handleOrgSelection({ organizationId: rootOrg.id, onSuccess });
} else { } else {
await navigateToBilling(); await navigateToBilling();
} }
}; };
const handleNavigateToAdminConsole = async () => {
const navigateToAdminConsole = () => {
navigate({
to: "/admin"
});
};
if (isSubOrganization) {
await handleOrgSelection({ organizationId: rootOrg.id, navigateTo: "/admin" });
} else {
navigateToAdminConsole();
}
};
const { mutateAsync } = useGetOrgTrialUrl(); const { mutateAsync } = useGetOrgTrialUrl();
const logout = useLogoutUser(); const logout = useLogoutUser();
@@ -368,14 +387,14 @@ export const Navbar = () => {
<button <button
className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap" className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap"
type="button" type="button"
onClick={async () => { onClick={() => {
if (isSubOrganization) {
handleOrgSelection({ organizationId: currentOrg.rootOrgId as string });
}
navigate({ navigate({
to: "/organizations/$orgId/projects", to: "/organizations/$orgId/projects",
params: { orgId: currentOrg.id } params: { orgId: currentOrg.id }
}); });
if (isSubOrganization) {
await router.invalidate({ sync: true }).catch(() => null);
}
}} }}
> >
<OrgIcon className={twMerge("size-[14px] shrink-0 text-org")} /> <OrgIcon className={twMerge("size-[14px] shrink-0 text-org")} />
@@ -467,9 +486,10 @@ export const Navbar = () => {
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
{Boolean(subOrganizations.length) && ( {Boolean(subOrganizations.length && !isSubOrganization) && (
<div className="mt-1 h-1 border-t border-mineshaft-600" /> <div className="mt-1 h-1 border-t border-mineshaft-600" />
)} )}
{!isSubOrganization && (
<DropdownMenuItem <DropdownMenuItem
className="cursor-pointer" className="cursor-pointer"
icon={<FontAwesomeIcon icon={faPlus} />} icon={<FontAwesomeIcon icon={faPlus} />}
@@ -477,6 +497,7 @@ export const Navbar = () => {
> >
New Sub-Organization New Sub-Organization
</DropdownMenuItem> </DropdownMenuItem>
)}{" "}
</DropdownSubMenuContent> </DropdownSubMenuContent>
</DropdownSubMenu> </DropdownSubMenu>
); );
@@ -509,24 +530,43 @@ export const Navbar = () => {
</div> </div>
{isSubOrganization && ( {isSubOrganization && (
<> <>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p> <ChevronRight size={18} className="mt-[3px] mr-3 text-mineshaft-400/70" />
<DropdownMenu modal={false}> <div
<Badge
asChild
isTruncatable
variant="sub-org"
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar
className={twMerge( className={twMerge(
"gap-x-1.5 text-sm", "relative flex min-w-16 items-center self-end rounded-t-md border-x border-t pt-1.5 pr-2 pb-2.5 pl-3",
isProjectScope && !isProjectScope && isSubOrganization
"min-w-6 bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-sub-org" ? "border-sub-org/15 bg-gradient-to-b from-sub-org/10 to-sub-org/[0.075]"
: "border-transparent"
)} )}
> >
<Link to="/organizations/$orgId/projects" params={{ orgId: currentOrg.id }}> {/* scott: the below is used to hide the top border from the org nav bar */}
<SubOrgIcon className="size-[12px]" /> {!isProjectScope && isSubOrganization && (
<span>{currentOrg.name}</span> <div className="absolute -bottom-px left-0 h-px w-full bg-mineshaft-900">
</Link> <div className="h-full bg-org/[0.075]" />
</div>
)}
<DropdownMenu modal={false}>
<div className="group mr-1 flex min-w-0 cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100">
<button
className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap"
type="button"
onClick={async () => {
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: currentOrg.id }
});
if (isSubOrganization) {
await router.invalidate({ sync: true }).catch(() => null);
}
}}
>
<SubOrgIcon className={twMerge("size-[14px] shrink-0 text-sub-org")} />
<span className="truncate">{currentOrg?.name}</span>
<Badge variant="sub-org" className="hidden lg:inline-flex">
Sub-Organization
</Badge> </Badge>
</button>
</div>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<div> <div>
<IconButton <IconButton
@@ -562,9 +602,10 @@ export const Navbar = () => {
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
{Boolean(subOrganizations.length) && ( {Boolean(subOrganizations.length && !isSubOrganization) && (
<div className="mt-1 h-1 border-t border-mineshaft-600" /> <div className="mt-1 h-1 border-t border-mineshaft-600" />
)} )}
{!isSubOrganization && (
<DropdownMenuItem <DropdownMenuItem
className="cursor-pointer" className="cursor-pointer"
icon={<FontAwesomeIcon icon={faPlus} />} icon={<FontAwesomeIcon icon={faPlus} />}
@@ -572,8 +613,10 @@ export const Navbar = () => {
> >
New Sub-Organization New Sub-Organization
</DropdownMenuItem> </DropdownMenuItem>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div>
</> </>
)} )}
{isProjectScope && ( {isProjectScope && (
@@ -618,6 +661,7 @@ export const Navbar = () => {
<Link <Link
className="mr-2 flex h-[34px] items-center rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600" className="mr-2 flex h-[34px] items-center rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
to="/admin" to="/admin"
onClick={handleNavigateToAdminConsole}
> >
<InstanceIcon className="inline-block size-3.5" /> <InstanceIcon className="inline-block size-3.5" />
<span className="ml-2 hidden md:inline-block">Server Console</span> <span className="ml-2 hidden md:inline-block">Server Console</span>
@@ -1,12 +1,16 @@
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useQueryClient } from "@tanstack/react-query";
import { useNavigate, useRouter } from "@tanstack/react-router"; import { useNavigate, useRouter } from "@tanstack/react-router";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, FormControl, Input } from "@app/components/v2"; import { Button, FormControl, Input } from "@app/components/v2";
import { useCreateSubOrganization } from "@app/hooks/api"; import { projectKeys, subOrganizationsQuery, useCreateSubOrganization } from "@app/hooks/api";
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
import { slugSchema } from "@app/lib/schemas"; import { slugSchema } from "@app/lib/schemas";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
type ContentProps = { type ContentProps = {
onClose: () => void; onClose: () => void;
@@ -20,6 +24,8 @@ type FormData = z.infer<typeof AddOrgSchema>;
export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
const createSubOrg = useCreateSubOrganization(); const createSubOrg = useCreateSubOrganization();
const subOrgQuery = subOrganizationsQuery.list({ limit: 500, isAccessible: true });
const queryClient = useQueryClient();
const { const {
handleSubmit, handleSubmit,
@@ -46,11 +52,18 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
}); });
onClose(); onClose();
navigate({ const { token } = await selectOrganization({
to: "/organizations/$orgId/projects", subOrganizationId: organization.id
params: { orgId: organization.id }
}); });
SecurityClient.setToken(token);
SecurityClient.setProviderAuthToken("");
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
await router.invalidate({ sync: true }).catch(() => null); await router.invalidate({ sync: true }).catch(() => null);
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
await navigateUserToOrg({ navigate, organizationId: organization.id });
}; };
return ( return (
@@ -5,7 +5,17 @@ import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { queryClient } from "@app/hooks/api/reactQuery"; import { queryClient } from "@app/hooks/api/reactQuery";
import { userKeys } from "@app/hooks/api/users"; import { userKeys } from "@app/hooks/api/users";
export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: string) => { type NavigateUserToOrgParams = {
navigate: NavigateFn;
organizationId?: string;
navigateTo?: string;
};
export const navigateUserToOrg = async ({
navigate,
organizationId,
navigateTo
}: NavigateUserToOrgParams) => {
const userOrgs = await fetchOrganizations(); const userOrgs = await fetchOrganizations();
const nonAuthEnforcedOrgs = userOrgs.filter((org) => !org.authEnforced); const nonAuthEnforcedOrgs = userOrgs.filter((org) => !org.authEnforced);
@@ -13,7 +23,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s
if (organizationId) { if (organizationId) {
localStorage.setItem("orgData.id", organizationId); localStorage.setItem("orgData.id", organizationId);
navigate({ navigate({
to: "/organizations/$orgId/projects", to: navigateTo || "/organizations/$orgId/projects",
params: { orgId: organizationId } params: { orgId: organizationId }
}); });
return; return;
@@ -24,7 +34,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s
const userOrg = nonAuthEnforcedOrgs[0] && nonAuthEnforcedOrgs[0].id; const userOrg = nonAuthEnforcedOrgs[0] && nonAuthEnforcedOrgs[0].id;
localStorage.setItem("orgData.id", userOrg); localStorage.setItem("orgData.id", userOrg);
navigate({ navigate({
to: "/organizations/$orgId/projects", to: navigateTo || "/organizations/$orgId/projects",
params: { orgId: userOrg } params: { orgId: userOrg }
}); });
} else { } else {
@@ -115,7 +115,7 @@ export const PasswordStep = ({
return; return;
} }
await navigateUserToOrg(navigate, organizationId); await navigateUserToOrg({ navigate, organizationId });
}; };
await finishWithOrgWorkflow(); await finishWithOrgWorkflow();
@@ -131,7 +131,7 @@ export const PasswordStep = ({
} }
// case: no orgs found, so we navigate the user to create an org // case: no orgs found, so we navigate the user to create an org
else { else {
await navigateUserToOrg(navigate); await navigateUserToOrg({ navigate });
} }
} }
} catch (err: any) { } catch (err: any) {
@@ -233,7 +233,7 @@ export const PasswordStep = ({
} }
// case: no orgs found, so we navigate the user to create an org // case: no orgs found, so we navigate the user to create an org
else { else {
await navigateUserToOrg(navigate); await navigateUserToOrg({ navigate });
} }
} }
} else { } else {
@@ -254,7 +254,7 @@ export const PasswordStep = ({
// case: organization ID is present from the provider auth token -- navigate directly to the org // case: organization ID is present from the provider auth token -- navigate directly to the org
if (organizationId) { if (organizationId) {
await navigateUserToOrg(navigate, organizationId); await navigateUserToOrg({ navigate, organizationId });
} }
// case: no organization ID is present -- navigate to the select org page IF the user has any orgs // case: no organization ID is present -- navigate to the select org page IF the user has any orgs
// if the user has no orgs, navigate to the create org page // if the user has no orgs, navigate to the create org page
@@ -264,7 +264,7 @@ export const PasswordStep = ({
if (userOrgs.length > 0) { if (userOrgs.length > 0) {
navigateToSelectOrganization(undefined, isAdminLogin); navigateToSelectOrganization(undefined, isAdminLogin);
} else { } else {
await navigateUserToOrg(navigate); await navigateUserToOrg({ navigate });
} }
} }
} }
@@ -316,7 +316,7 @@ export const PasswordStep = ({
return ( return (
<EmailDuplicationConfirmation <EmailDuplicationConfirmation
onRemoveDuplicateLater={() => onRemoveDuplicateLater={() =>
navigateUserToOrg(navigate, organizationId).catch(() => navigateUserToOrg({ navigate, organizationId }).catch(() =>
createNotification({ text: "Failed to navigate user", type: "error" }) createNotification({ text: "Failed to navigate user", type: "error" })
) )
} }
@@ -188,7 +188,7 @@ export const SelectOrganizationSection = () => {
navigate({ to: "/cli-redirect" }); navigate({ to: "/cli-redirect" });
// cli page // cli page
} else { } else {
navigateUserToOrg(navigate, organization.id); navigateUserToOrg({ navigate, organizationId: organization.id });
} }
}, },
[selectOrg] [selectOrg]
@@ -201,7 +201,7 @@ export const SelectOrganizationSection = () => {
const decodedJwt = jwtDecode(authToken) as any; const decodedJwt = jwtDecode(authToken) as any;
if (decodedJwt?.organizationId) { if (decodedJwt?.organizationId) {
navigateUserToOrg(navigate, decodedJwt.organizationId); navigateUserToOrg({ navigate, organizationId: decodedJwt.organizationId });
} }
} }