diff --git a/apps/web/components/apps/App.tsx b/apps/web/components/apps/App.tsx index 062e959b8e..2c8224a83b 100644 --- a/apps/web/components/apps/App.tsx +++ b/apps/web/components/apps/App.tsx @@ -1,23 +1,22 @@ import Link from "next/link"; import type { IframeHTMLAttributes } from "react"; -import React, { useState, useEffect } from "react"; -import { useRouter } from "next/router"; -import { CAL_URL } from "@calcom/lib/constants"; -import type { RouterOutputs } from "@calcom/trpc/react"; +import React, { useState } from "react"; import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation"; import { InstallAppButton, AppDependencyComponent } from "@calcom/app-store/components"; import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration"; +import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner"; import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams"; import Shell from "@calcom/features/shell/Shell"; import classNames from "@calcom/lib/classNames"; +import { CAL_URL } from "@calcom/lib/constants"; import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; -import type { App as AppType } from "@calcom/types/App"; +import type { App as AppType, AppFrontendPayload } from "@calcom/types/App"; import type { ButtonProps } from "@calcom/ui"; -import type { AppFrontendPayload as App } from "@calcom/types/App"; import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui"; import { Dropdown, @@ -28,9 +27,7 @@ import { DropdownItem, Avatar, } from "@calcom/ui"; -import { BookOpen, Check, ExternalLink, File, Flag, Mail, Plus, Shield } from "@calcom/ui/components/icon"; -import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner"; - +import { BookOpen, Check, ExternalLink, File, Flag, Mail, Shield } from "@calcom/ui/components/icon"; /* These app slugs all require Google Cal to be installed */ @@ -82,9 +79,11 @@ const Component = ({ { appType: type }, { onSettled(data) { - const credentialsCount = data?.credentials.length || 0 - setShowDisconnectIntegration(data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0); - setExistingCredentials(data?.credentials.map(credential => credential.id) || []); + const credentialsCount = data?.credentials.length || 0; + setShowDisconnectIntegration( + data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0 + ); + setExistingCredentials(data?.credentials.map((credential) => credential.id) || []); }, } ); @@ -186,7 +185,13 @@ const Component = ({ }; } return ( - + ); }} /> @@ -217,7 +222,13 @@ const Component = ({ }; } return ( - + ); }} /> @@ -398,13 +409,12 @@ const InstallAppButtonChild = ({ ...props }: { userAdminTeams?: UserAdminTeams; - addAppMutationInput: { type: App["type"]; variant: string; slug: string }; + addAppMutationInput: { type: AppFrontendPayload["type"]; variant: string; slug: string }; appCategories: string[]; multiInstall?: boolean; credentials?: RouterOutputs["viewer"]["appCredentialsByType"]["credentials"]; } & ButtonProps) => { const { t } = useLocale(); - const router = useRouter(); const mutation = useAddAppMutation(null, { onSuccess: (data) => { @@ -416,17 +426,21 @@ const InstallAppButtonChild = ({ }, }); - if (!userAdminTeams?.length || appCategories.some((category) => ["calendar", "conferencing"].includes(category))) { - return - + if ( + !userAdminTeams?.length || + appCategories.some((category) => ["calendar", "conferencing"].includes(category)) + ) { + return ( + + ); } return ( @@ -446,8 +460,7 @@ const InstallAppButtonChild = ({ { if (mutation.isLoading) event.preventDefault(); - }} - > + }}> {mutation.isLoading && (
@@ -455,20 +468,18 @@ const InstallAppButtonChild = ({ )} {t("install_app_on")} {userAdminTeams.map((team) => { - - const isInstalled = credentials && + const isInstalled = + credentials && credentials.some((credential) => credential?.teamId ? credential?.teamId === team.id : credential.userId === team.id - ) + ); return ( ( -

{team.name}{" "} - {isInstalled && - `(${t("installed")})`}

+

+ {team.name} {isInstalled && `(${t("installed")})`} +

- ) + ); })} diff --git a/packages/app-store/hubspot/api/callback.ts b/packages/app-store/hubspot/api/callback.ts index f4973b2ff5..58d544d645 100644 --- a/packages/app-store/hubspot/api/callback.ts +++ b/packages/app-store/hubspot/api/callback.ts @@ -47,7 +47,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // set expiry date as offset from current time. hubspotToken.expiryDate = Math.round(Date.now() + hubspotToken.expiresIn * 1000); - await createOAuthAppCredential({ appId: "hubspot", type: "hubspot_other_calendar" }, hubspotToken as any, req); + await createOAuthAppCredential( + { appId: "hubspot", type: "hubspot_other_calendar" }, + hubspotToken as any, + req + ); const state = decodeOAuthState(req); res.redirect( diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMemberUtils.test.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMemberUtils.test.ts index bf5cccce8a..da6a9a754a 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMemberUtils.test.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMemberUtils.test.ts @@ -1,11 +1,22 @@ import { describe, it, vi, expect } from "vitest"; -import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations"; -import { checkInputEmailIsValid, checkPermissions, getEmailsToInvite, getIsOrgVerified, getOrgConnectionInfo, throwIfInviteIsToOrgAndUserExists,createAndAutoJoinIfInOrg } from "./utils"; -import { MembershipRole } from "@calcom/prisma/enums"; + import { isTeamAdmin } from "@calcom/lib/server/queries"; -import { TRPCError } from "@trpc/server"; -import type { TeamWithParent } from "./types"; +import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations"; import type { User } from "@calcom/prisma/client"; +import { MembershipRole } from "@calcom/prisma/enums"; + +import { TRPCError } from "@trpc/server"; + +import type { TeamWithParent } from "./types"; +import { + checkInputEmailIsValid, + checkPermissions, + getEmailsToInvite, + getIsOrgVerified, + getOrgConnectionInfo, + throwIfInviteIsToOrgAndUserExists, + createAndAutoJoinIfInOrg, +} from "./utils"; vi.mock("@calcom/lib/server/queries", () => { return { @@ -19,7 +30,14 @@ vi.mock("@calcom/lib/server/queries/organisations", () => { }; }); -const mockedReturnSuccessCheckPerms = { accepted: true, disableImpersonation: false, id: 1, role: MembershipRole.ADMIN, userId: 1, teamId: 1 } +const mockedReturnSuccessCheckPerms = { + accepted: true, + disableImpersonation: false, + id: 1, + role: MembershipRole.ADMIN, + userId: 1, + teamId: 1, +}; const mockedTeam: TeamWithParent = { id: 1, @@ -41,20 +59,20 @@ const mockedTeam: TeamWithParent = { metadata: null, parentId: null, parent: null, - isPrivate:false + isPrivate: false, }; const mockUser: User = { id: 4, - username: 'pro', - name: 'Pro Example', - email: 'pro@example.com', + username: "pro", + name: "Pro Example", + email: "pro@example.com", emailVerified: new Date(), - password: '', + password: "", bio: null, avatar: null, - timeZone: 'Europe/London', - weekStart: 'Sunday', + timeZone: "Europe/London", + weekStart: "Sunday", startTime: 0, endTime: 1440, bufferTime: 0, @@ -64,78 +82,76 @@ const mockUser: User = { trialEndsAt: null, defaultScheduleId: null, completedOnboarding: true, - locale: 'en', + locale: "en", timeFormat: 12, twoFactorSecret: null, twoFactorEnabled: false, - identityProvider: 'CAL', + identityProvider: "CAL", identityProviderId: null, invitedTo: null, - brandColor: '#292929', - darkBrandColor: '#fafafa', + brandColor: "#292929", + darkBrandColor: "#fafafa", away: false, allowDynamicBooking: true, metadata: null, verified: false, - role: 'USER', + role: "USER", disableImpersonation: false, - organizationId: null -} + organizationId: null, +}; describe("Invite Member Utils", () => { describe("checkPermissions", () => { it("It should throw an error if the user is not an admin of the ORG", async () => { vi.mocked(isOrganisationAdmin).mockResolvedValue(false); await expect(checkPermissions({ userId: 1, teamId: 1, isOrg: true })).rejects.toThrow(); - }) + }); it("It should NOT throw an error if the user is an admin of the ORG", async () => { vi.mocked(isOrganisationAdmin).mockResolvedValue(mockedReturnSuccessCheckPerms); await expect(checkPermissions({ userId: 1, teamId: 1, isOrg: true })).resolves.not.toThrow(); - }) + }); it("It should throw an error if the user is not an admin of the team", async () => { vi.mocked(isTeamAdmin).mockResolvedValue(false); await expect(checkPermissions({ userId: 1, teamId: 1 })).rejects.toThrow(); - }) + }); it("It should NOT throw an error if the user is an admin of a team", async () => { vi.mocked(isTeamAdmin).mockResolvedValue(mockedReturnSuccessCheckPerms); await expect(checkPermissions({ userId: 1, teamId: 1 })).resolves.not.toThrow(); - }) - }) - describe('getEmailsToInvite', () => { - it('should throw a TRPCError with code BAD_REQUEST if no emails are provided', async () => { + }); + }); + describe("getEmailsToInvite", () => { + it("should throw a TRPCError with code BAD_REQUEST if no emails are provided", async () => { await expect(getEmailsToInvite([])).rejects.toThrow(TRPCError); }); - it('should return an array with one email if a string is provided', async () => { - const result = await getEmailsToInvite('test@example.com'); - expect(result).toEqual(['test@example.com']); + it("should return an array with one email if a string is provided", async () => { + const result = await getEmailsToInvite("test@example.com"); + expect(result).toEqual(["test@example.com"]); }); - it('should return an array with multiple emails if an array is provided', async () => { - const result = await getEmailsToInvite(['test1@example.com', 'test2@example.com']); - expect(result).toEqual(['test1@example.com', 'test2@example.com']); + it("should return an array with multiple emails if an array is provided", async () => { + const result = await getEmailsToInvite(["test1@example.com", "test2@example.com"]); + expect(result).toEqual(["test1@example.com", "test2@example.com"]); }); }); - describe('checkInputEmailIsValid', () => { - it('should throw a TRPCError with code BAD_REQUEST if the email is invalid', () => { - const invalidEmail = 'invalid-email'; + describe("checkInputEmailIsValid", () => { + it("should throw a TRPCError with code BAD_REQUEST if the email is invalid", () => { + const invalidEmail = "invalid-email"; expect(() => checkInputEmailIsValid(invalidEmail)).toThrow(TRPCError); expect(() => checkInputEmailIsValid(invalidEmail)).toThrowError( - 'Invite failed because invalid-email is not a valid email address' + "Invite failed because invalid-email is not a valid email address" ); }); - it('should not throw an error if the email is valid', () => { - const validEmail = 'valid-email@example.com'; + it("should not throw an error if the email is valid", () => { + const validEmail = "valid-email@example.com"; expect(() => checkInputEmailIsValid(validEmail)).not.toThrow(); }); }); describe("getOrgConnectionInfo", () => { - const orgAutoAcceptDomain = "example.com"; const usersEmail = "user@example.com"; - it("should return orgId and autoAccept as true if team has parent and usersEmail domain matches orgAutoAcceptDomain and orgVerified is true", () => { const result = getOrgConnectionInfo({ orgAutoAcceptDomain, @@ -264,7 +280,6 @@ describe("Invite Member Utils", () => { }; const isOrg = false; - it("should not throw when inviting an existing user to the same organization", () => { const inviteeWithOrg: User = { ...invitee, @@ -273,10 +288,8 @@ describe("Invite Member Utils", () => { const teamWithOrg = { ...mockedTeam, parentId: 2, - } - expect(() => - throwIfInviteIsToOrgAndUserExists(inviteeWithOrg, teamWithOrg, isOrg) - ).not.toThrow(); + }; + expect(() => throwIfInviteIsToOrgAndUserExists(inviteeWithOrg, teamWithOrg, isOrg)).not.toThrow(); }); it("should throw a TRPCError with code FORBIDDEN if the invitee is already a member of another organization", () => { const inviteeWithOrg: User = { @@ -286,23 +299,17 @@ describe("Invite Member Utils", () => { const teamWithOrg = { ...mockedTeam, parentId: 3, - } - expect(() => - throwIfInviteIsToOrgAndUserExists(inviteeWithOrg, teamWithOrg, isOrg) - ).toThrow(TRPCError); + }; + expect(() => throwIfInviteIsToOrgAndUserExists(inviteeWithOrg, teamWithOrg, isOrg)).toThrow(TRPCError); }); it("should throw a TRPCError with code FORBIDDEN if the invitee already exists in Cal.com and is being invited to an organization", () => { const isOrg = true; - expect(() => - throwIfInviteIsToOrgAndUserExists(invitee, mockedTeam, isOrg) - ).toThrow(TRPCError); + expect(() => throwIfInviteIsToOrgAndUserExists(invitee, mockedTeam, isOrg)).toThrow(TRPCError); }); it("should not throw an error if the invitee does not already belong to another organization and is not being invited to an organization", () => { - expect(() => - throwIfInviteIsToOrgAndUserExists(invitee, mockedTeam, isOrg) - ).not.toThrow(); + expect(() => throwIfInviteIsToOrgAndUserExists(invitee, mockedTeam, isOrg)).not.toThrow(); }); }); describe("createAndAutoJoinIfInOrg", () => { @@ -314,7 +321,7 @@ describe("Invite Member Utils", () => { }); expect(result).toEqual({ autoJoined: false }); }); - + it("should return autoJoined: false if the team does not have a parent organization", async () => { const result = await createAndAutoJoinIfInOrg({ team: { ...mockedTeam, parentId: null }, @@ -325,4 +332,4 @@ describe("Invite Member Utils", () => { }); // TODO: Add test for when the user is already a member of the organization - need to mock prisma response value }); -}) +});