Skip to content

Commit 973d83a

Browse files
PeerRichdevin-ai-integration[bot]dhairyashiil
authored
refactor: move organization members page from /settings/organizations/members to /members (calcom#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 (calcom#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>
1 parent 8d038f6 commit 973d83a

6 files changed

Lines changed: 202 additions & 120 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"use server";
2+
3+
import { revalidateTag } from "next/cache";
4+
5+
export async function revalidateAttributesList() {
6+
revalidateTag("viewer.attributes.list", "max");
7+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { UserListTableSkeleton } from "@calcom/web/modules/users/components/UserTable/UserListTableSkeleton";
2+
3+
export default function Loading() {
4+
return <UserListTableSkeleton />;
5+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
2+
3+
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
4+
5+
import { _generateMetadata, getTranslate } from "app/_utils";
6+
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
7+
import { cookies, headers } from "next/headers";
8+
import { redirect } from "next/navigation";
9+
10+
import { getOrgMembersPageData } from "~/members/getOrgMembersPageData";
11+
import { MembersView } from "~/members/members-view";
12+
13+
export const generateMetadata = async () =>
14+
await _generateMetadata(
15+
(t) => t("organization_members"),
16+
(t) => t("organization_description"),
17+
undefined,
18+
undefined,
19+
"/members"
20+
);
21+
22+
const Page = async () => {
23+
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
24+
25+
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
26+
return redirect("/settings/my-account/profile");
27+
}
28+
29+
const { org, teams, facetedTeamValues, attributes, permissions } = await getOrgMembersPageData(session);
30+
const t = await getTranslate();
31+
32+
return (
33+
<ShellMainAppDir heading={t("organization_members")} subtitle={t("organization_description")}>
34+
<MembersView
35+
org={org}
36+
teams={teams}
37+
facetedTeamValues={facetedTeamValues}
38+
attributes={attributes}
39+
permissions={permissions}
40+
/>
41+
</ShellMainAppDir>
42+
);
43+
};
44+
45+
export default Page;

apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx

Lines changed: 7 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,12 @@
1-
import { createRouterCaller } from "app/_trpc/context";
2-
import { _generateMetadata } from "app/_utils";
3-
import { unstable_cache } from "next/cache";
4-
import { headers, cookies } from "next/headers";
5-
import { redirect } from "next/navigation";
6-
7-
import { PrismaAttributeRepository } from "@calcom/features/attributes/repositories/PrismaAttributeRepository";
81
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
9-
import { Resource, CustomAction, CrudAction } from "@calcom/features/pbac/domain/types/permission-registry";
10-
import { getSpecificPermissions } from "@calcom/features/pbac/lib/resource-permissions";
11-
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
12-
import { prisma } from "@calcom/prisma";
13-
import { MembershipRole } from "@calcom/prisma/enums";
14-
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
152

163
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
174

5+
import { _generateMetadata } from "app/_utils";
6+
import { cookies, headers } from "next/headers";
7+
import { redirect } from "next/navigation";
8+
9+
import { getOrgMembersPageData } from "~/members/getOrgMembersPageData";
1810
import { MembersView } from "~/members/members-view";
1911

2012
export const generateMetadata = async () =>
@@ -26,127 +18,22 @@ export const generateMetadata = async () =>
2618
"/settings/organizations/members"
2719
);
2820

29-
const getCachedAttributes = unstable_cache(
30-
async (orgId: number) => {
31-
const attributeRepo = new PrismaAttributeRepository(prisma);
32-
33-
return await attributeRepo.findAllByOrgIdWithOptions({ orgId });
34-
},
35-
undefined,
36-
{ revalidate: 3600, tags: ["viewer.attributes.list"] } // Cache for 1 hour
37-
);
38-
39-
const getCachedRoles = unstable_cache(
40-
async (orgId: number) => {
41-
const roleManager = await RoleManagementFactory.getInstance().createRoleManager(orgId);
42-
return await roleManager.getAllRoles(orgId);
43-
},
44-
undefined,
45-
{ revalidate: 3600, tags: ["pbac.roles.list"] } // Cache for 1 hour
46-
);
47-
4821
const Page = async () => {
4922
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
5023

5124
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
5225
return redirect("/settings/profile");
5326
}
5427

55-
const orgCaller = await createRouterCaller(viewerOrganizationsRouter);
56-
const [org, teams] = await Promise.all([orgCaller.listCurrent(), orgCaller.getTeams()]);
57-
const [attributes, roles] = await Promise.all([getCachedAttributes(org.id), getCachedRoles(org.id)]);
58-
59-
const fallbackRolesThatCanSeeMembers: MembershipRole[] = [MembershipRole.ADMIN, MembershipRole.OWNER];
60-
61-
if (!org?.isPrivate) {
62-
fallbackRolesThatCanSeeMembers.push(MembershipRole.MEMBER);
63-
}
64-
65-
// Get specific PBAC permissions for organization member actions
66-
const [orgPermissions, attributesPermissions] = await Promise.all([
67-
getSpecificPermissions({
68-
userId: session.user.id,
69-
teamId: session.user.profile.organizationId,
70-
resource: Resource.Organization,
71-
userRole: session.user.org.role,
72-
actions: [
73-
CustomAction.ListMembers,
74-
CustomAction.ListMembersPrivate,
75-
CustomAction.Invite,
76-
CustomAction.ChangeMemberRole,
77-
CustomAction.Remove,
78-
CustomAction.Impersonate,
79-
],
80-
fallbackRoles: {
81-
[CustomAction.ListMembers]: {
82-
roles: fallbackRolesThatCanSeeMembers,
83-
},
84-
[CustomAction.ListMembersPrivate]: {
85-
roles: fallbackRolesThatCanSeeMembers,
86-
},
87-
[CustomAction.Invite]: {
88-
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
89-
},
90-
[CustomAction.ChangeMemberRole]: {
91-
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
92-
},
93-
[CustomAction.Remove]: {
94-
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
95-
},
96-
[CustomAction.Impersonate]: {
97-
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
98-
},
99-
},
100-
}),
101-
getSpecificPermissions({
102-
userId: session.user.id,
103-
teamId: session.user.profile.organizationId,
104-
resource: Resource.Attributes,
105-
userRole: session.user.org.role,
106-
actions: [CrudAction.Read, CustomAction.EditUsers],
107-
fallbackRoles: {
108-
[CrudAction.Read]: {
109-
roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
110-
},
111-
[CustomAction.EditUsers]: {
112-
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
113-
},
114-
},
115-
}),
116-
]);
117-
118-
// Map specific permissions to member actions
119-
const memberPermissions = {
120-
canListMembers: org.isPrivate
121-
? orgPermissions[CustomAction.ListMembersPrivate]
122-
: orgPermissions[CustomAction.ListMembers],
123-
canInvite: orgPermissions[CustomAction.Invite],
124-
canChangeMemberRole: orgPermissions[CustomAction.ChangeMemberRole],
125-
canRemove: orgPermissions[CustomAction.Remove],
126-
canImpersonate: orgPermissions[CustomAction.Impersonate],
127-
canViewAttributes: attributesPermissions[CrudAction.Read],
128-
canEditAttributesForUser: attributesPermissions[CustomAction.EditUsers],
129-
};
130-
131-
const facetedTeamValues = {
132-
roles,
133-
teams,
134-
attributes: attributes.map((attribute) => ({
135-
id: attribute.id,
136-
name: attribute.name,
137-
options: Array.from(new Set(attribute.options.map((option) => option.value))).map((value) => ({
138-
value,
139-
})),
140-
})),
141-
};
28+
const { org, teams, facetedTeamValues, attributes, permissions } = await getOrgMembersPageData(session);
14229

14330
return (
14431
<MembersView
14532
org={org}
14633
teams={teams}
14734
facetedTeamValues={facetedTeamValues}
14835
attributes={attributes}
149-
permissions={memberPermissions}
36+
permissions={permissions}
15037
/>
15138
);
15239
};
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { unstable_cache } from "next/cache";
2+
import { redirect } from "next/navigation";
3+
4+
import { PrismaAttributeRepository } from "@calcom/features/attributes/repositories/PrismaAttributeRepository";
5+
import type { Session } from "next-auth";
6+
import { CrudAction, CustomAction, Resource } from "@calcom/features/pbac/domain/types/permission-registry";
7+
import type { MemberPermissions } from "@calcom/features/pbac/lib/team-member-permissions";
8+
import { getSpecificPermissions } from "@calcom/features/pbac/lib/resource-permissions";
9+
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
10+
import { prisma } from "@calcom/prisma";
11+
import { MembershipRole } from "@calcom/prisma/enums";
12+
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
13+
14+
import { createRouterCaller } from "app/_trpc/context";
15+
16+
const getCachedAttributes = unstable_cache(
17+
async (orgId: number) => {
18+
const attributeRepo = new PrismaAttributeRepository(prisma);
19+
return await attributeRepo.findAllByOrgIdWithOptions({ orgId });
20+
},
21+
undefined,
22+
{ revalidate: 3600, tags: ["viewer.attributes.list"] }
23+
);
24+
25+
const getCachedRoles = unstable_cache(
26+
async (orgId: number) => {
27+
const roleManager = await RoleManagementFactory.getInstance().createRoleManager(orgId);
28+
return await roleManager.getAllRoles(orgId);
29+
},
30+
undefined,
31+
{ revalidate: 3600, tags: ["pbac.roles.list"] }
32+
);
33+
34+
export async function getOrgMembersPageData(session: Session) {
35+
const orgCaller = await createRouterCaller(viewerOrganizationsRouter);
36+
const [org, teams] = await Promise.all([orgCaller.listCurrent(), orgCaller.getTeams()]);
37+
38+
if (!org) {
39+
redirect("/settings/my-account/profile");
40+
}
41+
42+
const [attributes, roles] = await Promise.all([getCachedAttributes(org.id), getCachedRoles(org.id)]);
43+
44+
const fallbackRolesThatCanSeeMembers: MembershipRole[] = [MembershipRole.ADMIN, MembershipRole.OWNER];
45+
46+
if (!org?.isPrivate) {
47+
fallbackRolesThatCanSeeMembers.push(MembershipRole.MEMBER);
48+
}
49+
50+
const [orgPermissions, attributesPermissions] = await Promise.all([
51+
getSpecificPermissions({
52+
userId: session.user.id,
53+
teamId: session.user.profile!.organizationId!,
54+
resource: Resource.Organization,
55+
userRole: session.user.org!.role,
56+
actions: [
57+
CustomAction.ListMembers,
58+
CustomAction.ListMembersPrivate,
59+
CustomAction.Invite,
60+
CustomAction.ChangeMemberRole,
61+
CustomAction.Remove,
62+
CustomAction.Impersonate,
63+
],
64+
fallbackRoles: {
65+
[CustomAction.ListMembers]: {
66+
roles: fallbackRolesThatCanSeeMembers,
67+
},
68+
[CustomAction.ListMembersPrivate]: {
69+
roles: fallbackRolesThatCanSeeMembers,
70+
},
71+
[CustomAction.Invite]: {
72+
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
73+
},
74+
[CustomAction.ChangeMemberRole]: {
75+
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
76+
},
77+
[CustomAction.Remove]: {
78+
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
79+
},
80+
[CustomAction.Impersonate]: {
81+
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
82+
},
83+
},
84+
}),
85+
getSpecificPermissions({
86+
userId: session.user.id,
87+
teamId: session.user.profile!.organizationId!,
88+
resource: Resource.Attributes,
89+
userRole: session.user.org!.role,
90+
actions: [CrudAction.Read, CustomAction.EditUsers],
91+
fallbackRoles: {
92+
[CrudAction.Read]: {
93+
roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
94+
},
95+
[CustomAction.EditUsers]: {
96+
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
97+
},
98+
},
99+
}),
100+
]);
101+
102+
const permissions: MemberPermissions = {
103+
canListMembers: org.isPrivate
104+
? orgPermissions[CustomAction.ListMembersPrivate]
105+
: orgPermissions[CustomAction.ListMembers],
106+
canInvite: orgPermissions[CustomAction.Invite],
107+
canChangeMemberRole: orgPermissions[CustomAction.ChangeMemberRole],
108+
canRemove: orgPermissions[CustomAction.Remove],
109+
canImpersonate: orgPermissions[CustomAction.Impersonate],
110+
canViewAttributes: attributesPermissions[CrudAction.Read],
111+
canEditAttributesForUser: attributesPermissions[CustomAction.EditUsers],
112+
};
113+
114+
const facetedTeamValues = {
115+
roles,
116+
teams,
117+
attributes: attributes.map((attribute) => ({
118+
id: attribute.id,
119+
name: attribute.name,
120+
options: Array.from(new Set(attribute.options.map((option) => option.value))).map((value) => ({
121+
value,
122+
})),
123+
})),
124+
};
125+
126+
return {
127+
org,
128+
teams,
129+
facetedTeamValues,
130+
attributes,
131+
permissions,
132+
};
133+
}

apps/web/next.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,11 @@ const nextConfig = (phase: string): NextConfig => {
619619
destination: "/settings/platform",
620620
permanent: true,
621621
},
622+
{
623+
source: "/settings/organizations/members",
624+
destination: "/members",
625+
permanent: true,
626+
},
622627
{
623628
source: "/settings/admin/apps",
624629
destination: "/settings/admin/apps/calendar",

0 commit comments

Comments
 (0)