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>
This commit is contained in:
6 files changed
+202
-120
No files matched your search
@@ -0,0 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { revalidateTag } from "next/cache";
|
||||
|
||||
export async function revalidateAttributesList() {
|
||||
revalidateTag("viewer.attributes.list", "max");
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { UserListTableSkeleton } from "@calcom/web/modules/users/components/UserTable/UserListTableSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <UserListTableSkeleton />;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getOrgMembersPageData } from "~/members/getOrgMembersPageData";
|
||||
import { MembersView } from "~/members/members-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("organization_members"),
|
||||
(t) => t("organization_description"),
|
||||
undefined,
|
||||
undefined,
|
||||
"/members"
|
||||
);
|
||||
|
||||
const Page = async () => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
|
||||
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
|
||||
return redirect("/settings/my-account/profile");
|
||||
}
|
||||
|
||||
const { org, teams, facetedTeamValues, attributes, permissions } = await getOrgMembersPageData(session);
|
||||
const t = await getTranslate();
|
||||
|
||||
return (
|
||||
<ShellMainAppDir heading={t("organization_members")} subtitle={t("organization_description")}>
|
||||
<MembersView
|
||||
org={org}
|
||||
teams={teams}
|
||||
facetedTeamValues={facetedTeamValues}
|
||||
attributes={attributes}
|
||||
permissions={permissions}
|
||||
/>
|
||||
</ShellMainAppDir>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
+7
-120
@@ -1,20 +1,12 @@
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { headers, cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { PrismaAttributeRepository } from "@calcom/features/attributes/repositories/PrismaAttributeRepository";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { Resource, CustomAction, CrudAction } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
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 { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getOrgMembersPageData } from "~/members/getOrgMembersPageData";
|
||||
import { MembersView } from "~/members/members-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
@@ -26,25 +18,6 @@ export const generateMetadata = async () =>
|
||||
"/settings/organizations/members"
|
||||
);
|
||||
|
||||
const getCachedAttributes = unstable_cache(
|
||||
async (orgId: number) => {
|
||||
const attributeRepo = new PrismaAttributeRepository(prisma);
|
||||
|
||||
return await attributeRepo.findAllByOrgIdWithOptions({ orgId });
|
||||
},
|
||||
undefined,
|
||||
{ revalidate: 3600, tags: ["viewer.attributes.list"] } // Cache for 1 hour
|
||||
);
|
||||
|
||||
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"] } // Cache for 1 hour
|
||||
);
|
||||
|
||||
const Page = async () => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
|
||||
@@ -52,93 +25,7 @@ const Page = async () => {
|
||||
return redirect("/settings/profile");
|
||||
}
|
||||
|
||||
const orgCaller = await createRouterCaller(viewerOrganizationsRouter);
|
||||
const [org, teams] = await Promise.all([orgCaller.listCurrent(), orgCaller.getTeams()]);
|
||||
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);
|
||||
}
|
||||
|
||||
// Get specific PBAC permissions for organization member actions
|
||||
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],
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// Map specific permissions to member actions
|
||||
const 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,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
const { org, teams, facetedTeamValues, attributes, permissions } = await getOrgMembersPageData(session);
|
||||
|
||||
return (
|
||||
<MembersView
|
||||
@@ -146,7 +33,7 @@ const Page = async () => {
|
||||
teams={teams}
|
||||
facetedTeamValues={facetedTeamValues}
|
||||
attributes={attributes}
|
||||
permissions={memberPermissions}
|
||||
permissions={permissions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -619,6 +619,11 @@ const nextConfig = (phase: string): NextConfig => {
|
||||
destination: "/settings/platform",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/settings/organizations/members",
|
||||
destination: "/members",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/settings/admin/apps",
|
||||
destination: "/settings/admin/apps/calendar",
|
||||
|
||||
Reference in new issue
Block a user