Files
calendar/apps/web/modules/members/getOrgMembersPageData.ts
T
Peer RichelsenGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Dhairyashil Shinde
973d83aa57 refactor: move organization members page from /settings/organizations/members to /members (#27053)
* refactor: move organization members page from /settings/organizations/members to /members

- Create new /members route under main-nav with same functionality
- Add permanent redirect from /settings/organizations/members to /members
- Include loading skeleton and server actions for the new route

Co-Authored-By: peer@cal.com <peer@cal.com>

* refactor: Deduplicate organization members page logic (#27168)

* refactor(web): extract shared org members data fetching logic

- Extract data fetching, coaching, and permission logic into [getOrgMembersPageData.ts](cci:7://file:///Users/dhairyashilshinde/work/cal.com/apps/web/modules/members/getOrgMembersPageData.ts:0:0-0:0)
- Deduplicate logic between [/members](cci:7://file:///Users/dhairyashilshinde/work/cal.com/apps/web/app/%28use-page-wrapper%29/%28main-nav%29/members:0:0-0:0) and  pages
- Reduce page component size and improve maintainability

* safe guard for org id null

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
2026-01-22 22:43:34 +00:00

134 lines
4.6 KiB
TypeScript

import { unstable_cache } from "next/cache";
import { redirect } from "next/navigation";
import { PrismaAttributeRepository } from "@calcom/features/attributes/repositories/PrismaAttributeRepository";
import type { Session } from "next-auth";
import { CrudAction, CustomAction, Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import type { MemberPermissions } from "@calcom/features/pbac/lib/team-member-permissions";
import { getSpecificPermissions } from "@calcom/features/pbac/lib/resource-permissions";
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
import { createRouterCaller } from "app/_trpc/context";
const getCachedAttributes = unstable_cache(
async (orgId: number) => {
const attributeRepo = new PrismaAttributeRepository(prisma);
return await attributeRepo.findAllByOrgIdWithOptions({ orgId });
},
undefined,
{ revalidate: 3600, tags: ["viewer.attributes.list"] }
);
const getCachedRoles = unstable_cache(
async (orgId: number) => {
const roleManager = await RoleManagementFactory.getInstance().createRoleManager(orgId);
return await roleManager.getAllRoles(orgId);
},
undefined,
{ revalidate: 3600, tags: ["pbac.roles.list"] }
);
export async function getOrgMembersPageData(session: Session) {
const orgCaller = await createRouterCaller(viewerOrganizationsRouter);
const [org, teams] = await Promise.all([orgCaller.listCurrent(), orgCaller.getTeams()]);
if (!org) {
redirect("/settings/my-account/profile");
}
const [attributes, roles] = await Promise.all([getCachedAttributes(org.id), getCachedRoles(org.id)]);
const fallbackRolesThatCanSeeMembers: MembershipRole[] = [MembershipRole.ADMIN, MembershipRole.OWNER];
if (!org?.isPrivate) {
fallbackRolesThatCanSeeMembers.push(MembershipRole.MEMBER);
}
const [orgPermissions, attributesPermissions] = await Promise.all([
getSpecificPermissions({
userId: session.user.id,
teamId: session.user.profile!.organizationId!,
resource: Resource.Organization,
userRole: session.user.org!.role,
actions: [
CustomAction.ListMembers,
CustomAction.ListMembersPrivate,
CustomAction.Invite,
CustomAction.ChangeMemberRole,
CustomAction.Remove,
CustomAction.Impersonate,
],
fallbackRoles: {
[CustomAction.ListMembers]: {
roles: fallbackRolesThatCanSeeMembers,
},
[CustomAction.ListMembersPrivate]: {
roles: fallbackRolesThatCanSeeMembers,
},
[CustomAction.Invite]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
[CustomAction.ChangeMemberRole]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
[CustomAction.Remove]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
[CustomAction.Impersonate]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
},
}),
getSpecificPermissions({
userId: session.user.id,
teamId: session.user.profile!.organizationId!,
resource: Resource.Attributes,
userRole: session.user.org!.role,
actions: [CrudAction.Read, CustomAction.EditUsers],
fallbackRoles: {
[CrudAction.Read]: {
roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
},
[CustomAction.EditUsers]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
},
}),
]);
const permissions: MemberPermissions = {
canListMembers: org.isPrivate
? orgPermissions[CustomAction.ListMembersPrivate]
: orgPermissions[CustomAction.ListMembers],
canInvite: orgPermissions[CustomAction.Invite],
canChangeMemberRole: orgPermissions[CustomAction.ChangeMemberRole],
canRemove: orgPermissions[CustomAction.Remove],
canImpersonate: orgPermissions[CustomAction.Impersonate],
canViewAttributes: attributesPermissions[CrudAction.Read],
canEditAttributesForUser: attributesPermissions[CustomAction.EditUsers],
};
const facetedTeamValues = {
roles,
teams,
attributes: attributes.map((attribute) => ({
id: attribute.id,
name: attribute.name,
options: Array.from(new Set(attribute.options.map((option) => option.value))).map((value) => ({
value,
})),
})),
};
return {
org,
teams,
facetedTeamValues,
attributes,
permissions,
};
}