* Revert "Revert "feat: redesign team creation flow to match onboarding-v3 design (#26733)"" This re-applies the team creation redesign from PR #26733 which was previously reverted. Cherry-picked from 2540423ba3. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: check org/username slug when in org context * fix: use expect(page).toHaveURL() instead of page.waitForURL() in team e2e test Co-Authored-By: unknown <> * fix: redirect URL * fix: flag import --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
141 lines
4.7 KiB
TypeScript
141 lines
4.7 KiB
TypeScript
import { useFlagMap } from "@calcom/features/flags/context/provider";
|
|
import { CreationSource, MembershipRole } from "@calcom/prisma/enums";
|
|
import { trpc } from "@calcom/trpc/react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useState } from "react";
|
|
import type { OnboardingState } from "../store/onboarding-store";
|
|
import { useOnboardingStore } from "../store/onboarding-store";
|
|
|
|
type UseCreateTeamOptions = {
|
|
redirectBasePath?: string;
|
|
skipRedirectAfterInvite?: boolean;
|
|
};
|
|
|
|
export function useCreateTeam(options: UseCreateTeamOptions = {}) {
|
|
const { redirectBasePath = "/onboarding/teams", skipRedirectAfterInvite = false } = options;
|
|
const router = useRouter();
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const flags = useFlagMap();
|
|
const { setTeamId, teamId } = useOnboardingStore();
|
|
|
|
const createTeamMutation = trpc.viewer.teams.create.useMutation();
|
|
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation();
|
|
|
|
const createTeam = async (store?: OnboardingState) => {
|
|
setIsSubmitting(true);
|
|
|
|
try {
|
|
// Get the latest state from the store to ensure we have the most up-to-date values
|
|
const currentStore = store || useOnboardingStore.getState();
|
|
const { teamDetails, teamBrand } = currentStore;
|
|
|
|
// Validate team details - if empty, redirect back to team details step
|
|
if (!teamDetails.name || !teamDetails.name.trim() || !teamDetails.slug || !teamDetails.slug.trim()) {
|
|
router.push(`${redirectBasePath}/details`);
|
|
setIsSubmitting(false);
|
|
return;
|
|
}
|
|
|
|
// Create the team
|
|
const result = await createTeamMutation.mutateAsync({
|
|
name: teamDetails.name,
|
|
slug: teamDetails.slug,
|
|
bio: teamDetails.bio,
|
|
logo: teamBrand.logo,
|
|
isOnboarding: true,
|
|
});
|
|
|
|
// If there's a checkout URL, redirect to Stripe payment
|
|
if (result.url && !result.team) {
|
|
window.location.href = result.url;
|
|
return;
|
|
}
|
|
|
|
if (result.team) {
|
|
// Store the teamId and redirect to invite flow after team creation
|
|
setTeamId(result.team.id);
|
|
router.push(`${redirectBasePath}/invite/email?teamId=${result.team.id}`);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to create team:", error);
|
|
throw error;
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const inviteMembers = async (
|
|
invites: Array<{ email: string; role: "MEMBER" | "ADMIN" }>,
|
|
language: string
|
|
) => {
|
|
if (!teamId) {
|
|
throw new Error("Team ID is required to invite members");
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
|
|
try {
|
|
// Filter and validate invites
|
|
const validInvites = invites.filter((invite) => invite.email && invite.email.trim().length > 0);
|
|
|
|
if (validInvites.length === 0) {
|
|
throw new Error("At least one valid email address is required");
|
|
}
|
|
|
|
// Group invites by role and send separate requests for each role
|
|
// This is necessary because the schema validation expects array of strings when using bulk invites
|
|
const invitesByRole = validInvites.reduce(
|
|
(acc, invite) => {
|
|
const role = invite.role === "ADMIN" ? MembershipRole.ADMIN : MembershipRole.MEMBER;
|
|
if (!acc[role]) {
|
|
acc[role] = [];
|
|
}
|
|
acc[role].push(invite.email.trim().toLowerCase());
|
|
return acc;
|
|
},
|
|
{} as Record<MembershipRole, string[]>
|
|
);
|
|
|
|
// Send invites for each role group
|
|
await Promise.all(
|
|
Object.entries(invitesByRole).map(([role, emails]) =>
|
|
inviteMemberMutation.mutateAsync({
|
|
teamId,
|
|
usernameOrEmail: emails, // Array of strings, not objects
|
|
role: role as MembershipRole,
|
|
language,
|
|
creationSource: CreationSource.WEBAPP,
|
|
})
|
|
)
|
|
);
|
|
|
|
// Redirect to personal settings after successful invite (unless caller handles its own redirect)
|
|
if (!skipRedirectAfterInvite) {
|
|
const gettingStartedPath = flags["onboarding-v3"]
|
|
? "/onboarding/personal/settings?fromTeamOnboarding=true"
|
|
: "/getting-started";
|
|
router.replace(gettingStartedPath);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to invite members:", error);
|
|
// Extract error message from TRPC error
|
|
if (error && typeof error === "object" && "message" in error) {
|
|
throw new Error(error.message as string);
|
|
}
|
|
if (error instanceof Error) {
|
|
throw error;
|
|
}
|
|
throw new Error("Failed to invite members. Please try again.");
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return {
|
|
createTeam,
|
|
inviteMembers,
|
|
isSubmitting,
|
|
error: createTeamMutation.error || inviteMemberMutation.error,
|
|
};
|
|
}
|