fix: move organization members list (#17531)

Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
Eunjae Lee
2024-11-27 13:53:36 +00:00
committed by GitHub
co-authored by Omar López
parent afd730e1c8
commit 877e4199e7
17 changed files with 112 additions and 53 deletions
@@ -0,0 +1,14 @@
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import MembersPage from "~/members/members-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("organization_members"),
(t) => t("organization_description")
);
export default WithLayout({
Page: MembersPage,
});
@@ -2,7 +2,7 @@ import { _generateMetadata, getFixedT } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/members";
import LegacyPage from "@calcom/features/ee/organizations/pages/members";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import SettingsLayoutAppDir from "@calcom/features/settings/appDir/SettingsLayoutAppDir";
+26
View File
@@ -0,0 +1,26 @@
"use client";
import MembersView from "@calcom/features/ee/organizations/pages/members";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HeadSeo } from "@calcom/ui";
const MembersPage: React.FC = () => {
const { t } = useLocale();
return (
<Shell
withoutMain={false}
title={t("organization_members")}
description={t("organization_description")}
withoutSeo
heading={t("organization_members")}
hideHeadingOnMobile
subtitle={t("organization_description")}>
<HeadSeo title={t("organization_members")} description={t("organization_description")} />
<MembersView />
</Shell>
);
};
export default MembersPage;
@@ -529,6 +529,7 @@ test.describe("Bookings", () => {
const { invitedUserEmail } = await inviteExistingUserToOrganization({
page,
organizationId: org.id,
organizationSlug: org.slug,
user: userOutsideOrganization,
usersFixture: users,
});
@@ -6,15 +6,17 @@ import { submitAndWaitForResponse } from "../../lib/testUtils";
export const inviteUserToOrganization = async ({
page,
organizationId,
organizationSlug,
email,
usersFixture,
}: {
page: Page;
organizationId: number;
organizationSlug: string | null;
email: string;
usersFixture: ReturnType<typeof createUsersFixture>;
}) => {
await page.goto("/settings/organizations/members");
await page.goto(`/settings/organizations/${organizationSlug}/members`);
const invitedUserEmail = usersFixture.trackEmail({
username: email.split("@")[0],
domain: email.split("@")[1],
@@ -26,17 +28,19 @@ export const inviteUserToOrganization = async ({
export const inviteExistingUserToOrganization = async ({
page,
organizationId,
organizationSlug,
user,
usersFixture,
}: {
page: Page;
organizationId: number;
organizationSlug: string | null;
user: {
email: string;
};
usersFixture: ReturnType<typeof createUsersFixture>;
}) => {
await page.goto("/settings/organizations/members");
await page.goto(`/settings/organizations/${organizationSlug}/members`);
await inviteAnEmail(page, user.email);
return { invitedUserEmail: user.email };
};
@@ -24,7 +24,7 @@ test.describe("Organization", () => {
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true });
const { team: org } = await orgOwner.getOrgMembership();
await orgOwner.apiLogin();
await page.goto("/settings/organizations/members");
await page.goto(`/settings/organizations/${org.slug}/members`);
await test.step("By email", async () => {
const invitedUserEmail = users.trackEmail({ username: "rick", domain: "domain.com" });
@@ -42,6 +42,7 @@ test.describe("Organization", () => {
await expectUserToBeAMemberOfOrganization({
page,
orgSlug: org.slug,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: false,
@@ -61,6 +62,7 @@ test.describe("Organization", () => {
await expectUserToBeAMemberOfOrganization({
page,
orgSlug: org.slug,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
@@ -78,6 +80,7 @@ test.describe("Organization", () => {
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
await expectUserToBeAMemberOfOrganization({
page,
orgSlug: org.slug,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
@@ -119,6 +122,7 @@ test.describe("Organization", () => {
await expectUserToBeAMemberOfOrganization({
page,
orgSlug: org.slug,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: false,
@@ -156,6 +160,7 @@ test.describe("Organization", () => {
await expectUserToBeAMemberOfOrganization({
page,
orgSlug: org.slug,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
@@ -184,6 +189,7 @@ test.describe("Organization", () => {
await expectUserToBeAMemberOfOrganization({
page,
orgSlug: org.slug,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
@@ -203,7 +209,7 @@ test.describe("Organization", () => {
});
const { team: org } = await orgOwner.getOrgMembership();
await orgOwner.apiLogin();
await page.goto("/settings/organizations/members");
await page.goto(`/settings/organizations/${org.slug}/members`);
await test.step("By email", async () => {
const invitedUserEmail = users.trackEmail({ username: "rick", domain: "example.com" });
@@ -219,6 +225,7 @@ test.describe("Organization", () => {
await expectUserToBeAMemberOfOrganization({
page,
orgSlug: org.slug,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
@@ -238,6 +245,7 @@ test.describe("Organization", () => {
await expectUserToBeAMemberOfOrganization({
page,
orgSlug: org.slug,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
@@ -255,6 +263,7 @@ test.describe("Organization", () => {
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
await expectUserToBeAMemberOfOrganization({
page,
orgSlug: org.slug,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
@@ -346,6 +355,7 @@ test.describe("Organization", () => {
await expectUserToBeAMemberOfOrganization({
page,
orgSlug: org.slug,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
@@ -382,6 +392,7 @@ test.describe("Organization", () => {
await expectUserToBeAMemberOfOrganization({
page,
orgSlug: org.slug,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
@@ -411,6 +422,7 @@ test.describe("Organization", () => {
});
await expectUserToBeAMemberOfOrganization({
page,
orgSlug: org.slug,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
@@ -542,19 +554,21 @@ async function inviteAnEmail(page: Page, invitedUserEmail: string, teamPage?: bo
async function expectUserToBeAMemberOfOrganization({
page,
orgSlug,
username,
email,
role,
isMemberShipAccepted,
}: {
page: Page;
orgSlug: string | null;
username: string;
role: string;
isMemberShipAccepted: boolean;
email: string;
}) {
// Check newly invited member is not pending anymore
await page.goto("/settings/organizations/members");
await page.goto(`/settings/organizations/${orgSlug}/members`);
await expect(page.locator(`[data-testid="member-${username}-username"]`)).toHaveText(username);
await expect(page.locator(`[data-testid="member-${username}-email"]`)).toHaveText(email);
expect((await page.locator(`[data-testid="member-${username}-role"]`).textContent())?.toLowerCase()).toBe(
@@ -46,7 +46,7 @@ test.describe("Organization - Privacy", () => {
});
await owner.apiLogin();
await page.goto("/settings/organizations/members");
await page.goto(`/settings/organizations/${org.slug}/members`);
await page.waitForLoadState("domcontentloaded");
const tableLocator = await page.getByTestId("user-list-data-table");
@@ -54,7 +54,7 @@ test.describe("Organization - Privacy", () => {
await expect(tableLocator).toBeVisible();
await memberInOrg.apiLogin();
await page.goto("/settings/organizations/members");
await page.goto(`/settings/organizations/${org.slug}/members`);
await page.waitForLoadState("domcontentloaded");
const userDataTable = await page.getByTestId("user-list-data-table");
const membersPrivacyWarning = await page.getByTestId("members-privacy-warning");
@@ -17,11 +17,7 @@ const PoweredByCal = ({ logoOnly }: { logoOnly?: boolean }) => {
{!logoOnly && <>{t("powered_by")} </>}
{APP_NAME === "Cal.com" || !hasValidLicense ? (
<>
<img
className="-mt-px inline h-[10px] w-auto dark:invert"
src="/api/logo"
alt="Cal.com Logo"
/>
<img className="-mt-px inline h-[10px] w-auto dark:invert" src="/api/logo" alt="Cal.com Logo" />
</>
) : (
<span className="text-emphasis font-semibold opacity-50 hover:opacity-100">{APP_NAME}</span>
@@ -5,7 +5,6 @@ import { UserListTable } from "@calcom/features/users/components/UserTable/UserL
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { Meta } from "@calcom/ui";
const MembersView = () => {
const { t } = useLocale();
@@ -16,7 +16,6 @@ import {
Button,
Form,
Label,
Meta,
Select,
showToast,
SkeletonButton,
@@ -1,7 +1,6 @@
"use client";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Meta } from "@calcom/ui";
import { OtherTeamsListing } from "./../components/OtherTeamsListing";
@@ -31,7 +31,6 @@ import {
ImageUploader,
Label,
LinkIconButton,
Meta,
showToast,
SkeletonContainer,
SkeletonText,
@@ -27,7 +27,6 @@ import {
ImageUploader,
Label,
LinkIconButton,
Meta,
showToast,
SkeletonAvatar,
SkeletonButton,
@@ -4,7 +4,7 @@ import { useSession } from "next-auth/react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MembershipRole } from "@calcom/prisma/enums";
import { AppSkeletonLoader as SkeletonLoader, Meta } from "@calcom/ui";
import { AppSkeletonLoader as SkeletonLoader } from "@calcom/ui";
import SSOConfiguration from "../components/SSOConfiguration";
@@ -78,10 +78,6 @@ const tabs: VerticalTabItemProps[] = [
name: "general",
href: "/settings/organizations/general",
},
{
name: "members",
href: "/settings/organizations/members",
},
{
name: "privacy",
href: "/settings/organizations/privacy",
@@ -77,10 +77,6 @@ const tabs: VerticalTabItemProps[] = [
name: "general",
href: "/settings/organizations/general",
},
{
name: "members",
href: "/settings/organizations/members",
},
{
name: "privacy",
href: "/settings/organizations/privacy",
@@ -1,7 +1,12 @@
import { useSession } from "next-auth/react";
import { useMemo } from "react";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import UnconfirmedBookingBadge from "@calcom/features/bookings/UnconfirmedBookingBadge";
import {
useOrgBranding,
type OrganizationBranding,
} from "@calcom/features/ee/organizations/context/provider";
import { KBarTrigger } from "@calcom/features/kbar/Kbar";
import { classNames } from "@calcom/lib";
@@ -11,7 +16,7 @@ import { NavigationItem, MobileNavigationItem, MobileNavigationMoreItem } from "
export const MORE_SEPARATOR_NAME = "more";
const navigation: NavigationItemType[] = [
const getNavigationItems = (orgBranding: OrganizationBranding): NavigationItemType[] => [
{
name: "event_types_page_title",
href: "/event-types",
@@ -29,6 +34,15 @@ const navigation: NavigationItemType[] = [
href: "/availability",
icon: "clock",
},
...(orgBranding
? [
{
name: "members",
href: `/settings/organizations/${orgBranding.slug}/members`,
icon: "building",
} satisfies NavigationItemType,
]
: []),
{
name: "teams",
href: "/teams",
@@ -102,7 +116,7 @@ const navigation: NavigationItemType[] = [
},
];
const platformNavigation: NavigationItemType[] = [
const platformNavigationItems: NavigationItemType[] = [
{
name: "Dashboard",
href: "/settings/platform/",
@@ -144,33 +158,36 @@ const platformNavigation: NavigationItemType[] = [
},
];
export const getDesktopNavigationItems = (isPlatformNavigation = false) => {
const navigationType = !isPlatformNavigation ? navigation : platformNavigation;
const moreSeparatorIndex = navigationType.findIndex((item) => item.name === MORE_SEPARATOR_NAME);
const useNavigationItems = (isPlatformNavigation = false) => {
const orgBranding = useOrgBranding();
return useMemo(() => {
const navigationType = !isPlatformNavigation ? getNavigationItems(orgBranding) : platformNavigationItems;
const moreSeparatorIndex = navigationType.findIndex((item) => item.name === MORE_SEPARATOR_NAME);
const { desktopNavigationItems, mobileNavigationBottomItems, mobileNavigationMoreItems } = (
!isPlatformNavigation ? navigation : platformNavigation
).reduce<Record<string, NavigationItemType[]>>(
(items, item, index) => {
// We filter out the "more" separator in` desktop navigation
if (item.name !== MORE_SEPARATOR_NAME) items.desktopNavigationItems.push(item);
// Items for mobile bottom navigation
if (index < moreSeparatorIndex + 1 && !item.onlyDesktop) {
items.mobileNavigationBottomItems.push(item);
} // Items for the "more" menu in mobile navigation
else {
items.mobileNavigationMoreItems.push(item);
}
return items;
},
{ desktopNavigationItems: [], mobileNavigationBottomItems: [], mobileNavigationMoreItems: [] }
);
const { desktopNavigationItems, mobileNavigationBottomItems, mobileNavigationMoreItems } = (
!isPlatformNavigation ? getNavigationItems(orgBranding) : platformNavigationItems
).reduce<Record<string, NavigationItemType[]>>(
(items, item, index) => {
// We filter out the "more" separator in` desktop navigation
if (item.name !== MORE_SEPARATOR_NAME) items.desktopNavigationItems.push(item);
// Items for mobile bottom navigation
if (index < moreSeparatorIndex + 1 && !item.onlyDesktop) {
items.mobileNavigationBottomItems.push(item);
} // Items for the "more" menu in mobile navigation
else {
items.mobileNavigationMoreItems.push(item);
}
return items;
},
{ desktopNavigationItems: [], mobileNavigationBottomItems: [], mobileNavigationMoreItems: [] }
);
return { desktopNavigationItems, mobileNavigationBottomItems, mobileNavigationMoreItems };
return { desktopNavigationItems, mobileNavigationBottomItems, mobileNavigationMoreItems };
}, [isPlatformNavigation, orgBranding]);
};
export const Navigation = ({ isPlatformNavigation = false }: { isPlatformNavigation?: boolean }) => {
const { desktopNavigationItems } = getDesktopNavigationItems(isPlatformNavigation);
const { desktopNavigationItems } = useNavigationItems(isPlatformNavigation);
return (
<nav className="mt-2 flex-1 md:px-2 lg:mt-4 lg:px-0">
@@ -196,7 +213,7 @@ export function MobileNavigationContainer({
const MobileNavigation = ({ isPlatformNavigation = false }: { isPlatformNavigation?: boolean }) => {
const isEmbed = useIsEmbed();
const { mobileNavigationBottomItems } = getDesktopNavigationItems(isPlatformNavigation);
const { mobileNavigationBottomItems } = useNavigationItems(isPlatformNavigation);
return (
<>
@@ -216,7 +233,7 @@ const MobileNavigation = ({ isPlatformNavigation = false }: { isPlatformNavigati
};
export const MobileNavigationMoreItems = () => {
const { mobileNavigationMoreItems } = getDesktopNavigationItems();
const { mobileNavigationMoreItems } = useNavigationItems();
return (
<ul className="border-subtle mt-2 rounded-md border">