diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/members/actions.ts b/apps/web/app/(use-page-wrapper)/(main-nav)/members/actions.ts
new file mode 100644
index 0000000000..1263028097
--- /dev/null
+++ b/apps/web/app/(use-page-wrapper)/(main-nav)/members/actions.ts
@@ -0,0 +1,7 @@
+"use server";
+
+import { revalidateTag } from "next/cache";
+
+export async function revalidateAttributesList() {
+ revalidateTag("viewer.attributes.list", "max");
+}
diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/members/loading.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/members/loading.tsx
new file mode 100644
index 0000000000..58084f89a3
--- /dev/null
+++ b/apps/web/app/(use-page-wrapper)/(main-nav)/members/loading.tsx
@@ -0,0 +1,5 @@
+import { UserListTableSkeleton } from "@calcom/web/modules/users/components/UserTable/UserListTableSkeleton";
+
+export default function Loading() {
+ return ;
+}
diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/members/page.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/members/page.tsx
new file mode 100644
index 0000000000..ecc8bddcda
--- /dev/null
+++ b/apps/web/app/(use-page-wrapper)/(main-nav)/members/page.tsx
@@ -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 (
+
+
+
+ );
+};
+
+export default Page;
diff --git a/apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx b/apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx
index 98147f9277..3e3e6b745b 100644
--- a/apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx
@@ -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 (
{
teams={teams}
facetedTeamValues={facetedTeamValues}
attributes={attributes}
- permissions={memberPermissions}
+ permissions={permissions}
/>
);
};
diff --git a/apps/web/modules/members/getOrgMembersPageData.ts b/apps/web/modules/members/getOrgMembersPageData.ts
new file mode 100644
index 0000000000..96df71a0ae
--- /dev/null
+++ b/apps/web/modules/members/getOrgMembersPageData.ts
@@ -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,
+ };
+}
diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts
index b02ebd2916..1cf874c6a4 100644
--- a/apps/web/next.config.ts
+++ b/apps/web/next.config.ts
@@ -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",