Files
calendar/packages/trpc/server/routers/viewer/admin/moveTeamToOrg.handler.ts
T
Hariom BalharaGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>bot_apk
dc43eba23b feat: add Move Team to Organization admin migration page (#25067)
* feat: restore moveTeamToOrg admin endpoint for organization migration

- Add moveTeamToOrg and removeTeamFromOrg functions to orgMigration.ts
- Restore API endpoint at /api/orgMigration/moveTeamToOrg
- Restore admin UI page at /settings/admin/orgMigrations/moveTeamToOrg
- Add helper functions for team redirect management
- Support moving team members along with the team

This endpoint allows admins to migrate teams to organizations after org creation,
which is needed as a temporary solution until proper org admin permissions are implemented.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: move moveTeamToOrg to lib/orgMigration and fix redirect URL

- Move moveTeamToOrg and removeTeamFromOrg functions from playwright/lib to lib/orgMigration.ts
- Update API endpoint to import from lib/orgMigration instead of playwright/lib
- Fix redirect URL format: use / instead of /team/
- Fix import path: use ../playwright/lib/orgMigration instead of ./playwright/lib/orgMigration
- Rename unused _dbRemoveTeamFromOrg in playwright file to satisfy linter
- Remove duplicate functions from playwright/lib/orgMigration.ts

This fixes the Vercel deployment failure caused by importing from test-only directories
in production API routes, and corrects the redirect URL format to match the original implementation.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: reuse existing createTeamsHandler for moveTeamToOrg endpoint

- Remove custom orgMigration.ts implementation
- Update API endpoint to call existing createTeamsHandler with org owner impersonation
- Remove moveMembers option from UI (always moves members by design)
- Fix Vercel deployment by removing playwright import from production code
- Use OrganizationRepository.adminFindById to fetch org owner

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: migrate moveTeamToOrg admin page and API to App Router

- Move admin page from pages/settings/admin/orgMigrations to app/(use-page-wrapper)/settings/(admin-layout)/admin/orgMigrations
- Convert API route from pages/api/orgMigration/moveTeamToOrg.ts to app/api/orgMigration/moveTeamToOrg/route.ts
- Create client view component in modules/settings/admin/org-migrations/
- Remove old pages directory files and getServerSideProps

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: correct import paths for App Router compatibility

- Fix @calcom/lib/server to @calcom/lib/server/i18n for getTranslation
- Fix @calcom/ui barrel import to specific component paths

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: use TFunction type for getFormSchema parameter

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: use buildLegacyRequest for App Router session compatibility

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Remove unused fn

* cleanup

* cleanup

* fix: ui

* fix: handle slug conflict error when moving team to organization

- Intercept Prisma P2002 unique constraint error when moving a team
- Convert to user-friendly CONFLICT error with clear message
- Add test case for slug conflict scenario

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: use isPending instead of isLoading for tRPC mutation

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fixes

* fix: remove PII (emails) from admin log statement

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: use instanceof pattern for Prisma error detection

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fixes

* fix: use i18n key for slug conflict error message instead of hardcoded English string

Co-Authored-By: bot_apk <apk@cognition.ai>

* fix: narrow P2002 catch scope to only prisma.team.update call

Separates the try-catch for prisma.team.update (slug conflict) from
creditService.moveCreditsFromTeamToOrg to avoid misattributing credit
service P2002 errors as slug conflicts.

Co-Authored-By: bot_apk <apk@cognition.ai>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: bot_apk <apk@cognition.ai>
2026-03-19 09:55:02 +05:30

114 lines
2.9 KiB
TypeScript

import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container";
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import slugify from "@calcom/lib/slugify";
import { CreationSource } from "@calcom/prisma/enums";
import { createTeamsHandler } from "@calcom/trpc/server/routers/viewer/organizations/createTeams.handler";
import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../types";
import type { TMoveTeamToOrg } from "./moveTeamToOrg.schema";
const log = logger.getSubLogger({ prefix: ["moveTeamToOrg"] });
type MoveTeamToOrgOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
prisma: typeof import("@calcom/prisma").prisma;
};
input: TMoveTeamToOrg;
};
export const moveTeamToOrgHandler = async ({ ctx, input }: MoveTeamToOrgOptions) => {
const { teamId, targetOrgId, teamSlugInOrganization } = input;
log.debug(
"Moving team to org:",
safeStringify({
teamId,
targetOrgId,
teamSlugInOrganization,
})
);
const organizationRepository = getOrganizationRepository();
let org;
try {
org = await organizationRepository.adminFindById({ id: targetOrgId });
} catch (error) {
throw new TRPCError({ code: "NOT_FOUND", message: "organization_not_found" });
}
if (!org.members || org.members.length === 0) {
throw new TRPCError({ code: "BAD_REQUEST", message: "organization_owner_not_found" });
}
const teamRepository = new TeamRepository(ctx.prisma);
const team = await teamRepository.findById({ id: teamId });
if (!team) {
throw new TRPCError({ code: "NOT_FOUND", message: "team_not_found" });
}
if (team.parentId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "cannot_move_subteam_already_in_org",
});
}
if (team.isOrganization) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "cannot_move_organization_to_organization",
});
}
const orgOwner = org.members[0].user;
log.info(
"Admin moving team to organization",
safeStringify({
adminUserId: ctx.user.id,
orgOwnerId: orgOwner.id,
teamId,
targetOrgId,
})
);
const oldTeamSlug = team.slug;
const newTeamSlug = slugify(teamSlugInOrganization);
await createTeamsHandler({
ctx: {
user: {
id: orgOwner.id,
organizationId: targetOrgId,
},
},
input: {
teamNames: [],
orgId: targetOrgId,
moveTeams: [
{
id: teamId,
newSlug: newTeamSlug,
shouldMove: true,
},
],
creationSource: CreationSource.WEBAPP,
},
});
return {
message: "team_moved_to_org_success",
teamId,
oldTeamSlug,
newTeamSlug,
organizationId: targetOrgId,
organizationSlug: org.slug,
};
};
export default moveTeamToOrgHandler;