From ba4e717b599e4a48e18815b1d3a1590e035ec235 Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Thu, 7 Sep 2023 21:21:04 -0300 Subject: [PATCH 01/11] chore: unify org data access (#11158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Peer Richelsen Co-authored-by: Omar López --- apps/web/lib/app-providers.tsx | 15 +--------- .../features/auth/lib/getServerSession.ts | 2 +- .../features/auth/lib/next-auth-options.ts | 29 +++++++++++++++---- .../ee/organizations/context/provider.ts | 2 +- .../ee/organizations/pages/organization.tsx | 4 +-- .../settings/other-team-members-view.tsx | 2 +- .../ee/teams/components/AddNewTeamMembers.tsx | 2 +- .../ee/teams/pages/team-members-view.tsx | 2 +- .../settings/layouts/SettingsLayout.tsx | 4 +-- packages/features/shell/Shell.tsx | 12 ++++---- .../UserTable/ChangeUserRoleModal.tsx | 2 +- .../UserTable/DeleteMemberModal.tsx | 4 +-- .../UserTable/ImpersonationMemberModal.tsx | 2 +- .../UserTable/InviteMemberModal.tsx | 4 +-- packages/types/next-auth.d.ts | 15 +++++++++- 15 files changed, 59 insertions(+), 42 deletions(-) diff --git a/apps/web/lib/app-providers.tsx b/apps/web/lib/app-providers.tsx index 329f88070d..2c81796c72 100644 --- a/apps/web/lib/app-providers.tsx +++ b/apps/web/lib/app-providers.tsx @@ -14,7 +14,6 @@ import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/ import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic"; import { FeatureProvider } from "@calcom/features/flags/context/provider"; import { useFlags } from "@calcom/features/flags/hooks"; -import { trpc } from "@calcom/trpc/react"; import { MetaProvider } from "@calcom/ui"; import useIsBookingPage from "@lib/hooks/useIsBookingPage"; @@ -222,19 +221,7 @@ function FeatureFlagsProvider({ children }: { children: React.ReactNode }) { function useOrgBrandingValues() { const session = useSession(); - - const res = trpc.viewer.organizations.getBrand.useQuery(undefined, { - // Only fetch if we have a session to avoid flooding logs with errors - enabled: session.status === "authenticated", - }); - - if (res.status === "loading") { - return undefined; - } - - if (res.status === "error") return null; - - return res.data; + return session?.data?.user.org; } function OrgBrandProvider({ children }: { children: React.ReactNode }) { diff --git a/packages/features/auth/lib/getServerSession.ts b/packages/features/auth/lib/getServerSession.ts index a2a53e9117..029c9f5e8e 100644 --- a/packages/features/auth/lib/getServerSession.ts +++ b/packages/features/auth/lib/getServerSession.ts @@ -72,7 +72,7 @@ export async function getServerSession(options: { image: `${CAL_URL}/${user.username}/avatar.png`, impersonatedByUID: token.impersonatedByUID ?? undefined, belongsToActiveTeam: token.belongsToActiveTeam, - organizationId: token.organizationId, + org: token.org, locale: user.locale ?? undefined, }, }; diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts index 3c7ab80d80..b101ccfe74 100644 --- a/packages/features/auth/lib/next-auth-options.ts +++ b/packages/features/auth/lib/next-auth-options.ts @@ -8,6 +8,7 @@ import GoogleProvider from "next-auth/providers/google"; import checkLicense from "@calcom/features/ee/common/server/checkLicense"; import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider"; +import { getOrgFullDomain, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains"; import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; @@ -402,7 +403,14 @@ export const AUTH_OPTIONS: AuthOptions = { username: true, name: true, email: true, - organizationId: true, + organization: { + select: { + id: true, + name: true, + slug: true, + metadata: true, + }, + }, role: true, locale: true, teams: { @@ -419,12 +427,23 @@ export const AUTH_OPTIONS: AuthOptions = { // Check if the existingUser has any active teams const belongsToActiveTeam = checkIfUserBelongsToActiveTeam(existingUser); - const { teams: _teams, ...existingUserWithoutTeamsField } = existingUser; + const { teams: _teams, organization, ...existingUserWithoutTeamsField } = existingUser; + + const parsedOrgMetadata = teamMetadataSchema.parse(organization?.metadata ?? {}); return { ...existingUserWithoutTeamsField, ...token, belongsToActiveTeam, + org: organization + ? { + id: organization.id, + name: organization.name, + slug: organization.slug ?? parsedOrgMetadata?.requestedSlug ?? "", + fullDomain: getOrgFullDomain(organization.slug ?? parsedOrgMetadata?.requestedSlug ?? ""), + domainSuffix: subdomainSuffix(), + } + : undefined, }; }; if (!user) { @@ -448,7 +467,7 @@ export const AUTH_OPTIONS: AuthOptions = { role: user.role, impersonatedByUID: user?.impersonatedByUID, belongsToActiveTeam: user?.belongsToActiveTeam, - organizationId: user?.organizationId, + org: user?.org, locale: user?.locale, }; } @@ -487,7 +506,7 @@ export const AUTH_OPTIONS: AuthOptions = { role: existingUser.role, impersonatedByUID: token.impersonatedByUID as number, belongsToActiveTeam: token?.belongsToActiveTeam as boolean, - organizationId: token?.organizationId, + org: token?.org, locale: existingUser.locale, }; } @@ -507,7 +526,7 @@ export const AUTH_OPTIONS: AuthOptions = { role: token.role as UserPermissionRole, impersonatedByUID: token.impersonatedByUID as number, belongsToActiveTeam: token?.belongsToActiveTeam as boolean, - organizationId: token?.organizationId, + org: token?.org, locale: token.locale, }, }; diff --git a/packages/features/ee/organizations/context/provider.ts b/packages/features/ee/organizations/context/provider.ts index bd09a395c8..2cee02224f 100644 --- a/packages/features/ee/organizations/context/provider.ts +++ b/packages/features/ee/organizations/context/provider.ts @@ -10,7 +10,7 @@ import type { teamMetadataSchema } from "@calcom/prisma/zod-utils"; */ export type OrganizationBranding = | ({ - logo?: string | null | undefined; + id: number; name?: string; slug: string; fullDomain: string; diff --git a/packages/features/ee/organizations/pages/organization.tsx b/packages/features/ee/organizations/pages/organization.tsx index 6f93a2610d..b9114d70da 100644 --- a/packages/features/ee/organizations/pages/organization.tsx +++ b/packages/features/ee/organizations/pages/organization.tsx @@ -16,7 +16,7 @@ export const getServerSideProps = async ({ req, res }: GetServerSidePropsContext // Check if logged in user has an organization assigned const session = await getServerSession({ req, res }); - if (!session?.user.organizationId) { + if (!session?.user.org?.id) { return { notFound: true, }; @@ -26,7 +26,7 @@ export const getServerSideProps = async ({ req, res }: GetServerSidePropsContext const membership = await prisma.membership.findFirst({ where: { userId: session?.user.id, - teamId: session?.user.organizationId, + teamId: session?.user.org.id, }, select: { role: true, diff --git a/packages/features/ee/organizations/pages/settings/other-team-members-view.tsx b/packages/features/ee/organizations/pages/settings/other-team-members-view.tsx index 4eb9139696..0e026e8127 100644 --- a/packages/features/ee/organizations/pages/settings/other-team-members-view.tsx +++ b/packages/features/ee/organizations/pages/settings/other-team-members-view.tsx @@ -69,7 +69,7 @@ const MembersView = () => { const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false); const [members, setMembers] = useState([]); const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { - enabled: !!session.data?.user?.organizationId, + enabled: !!session.data?.user?.org, }); const { data: team, isLoading: isTeamLoading } = trpc.viewer.organizations.getOtherTeam.useQuery( { teamId }, diff --git a/packages/features/ee/teams/components/AddNewTeamMembers.tsx b/packages/features/ee/teams/components/AddNewTeamMembers.tsx index 917d06e537..c1c7902fa5 100644 --- a/packages/features/ee/teams/components/AddNewTeamMembers.tsx +++ b/packages/features/ee/teams/components/AddNewTeamMembers.tsx @@ -215,7 +215,7 @@ const PendingMemberItem = (props: { member: TeamMember; index: number; teamId: n const session = useSession(); const bookerUrl = useBookerUrl(); const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { - enabled: !!session.data?.user?.organizationId, + enabled: !!session.data?.user?.org, }); const removeMemberMutation = trpc.viewer.teams.removeMember.useMutation({ async onSuccess() { diff --git a/packages/features/ee/teams/pages/team-members-view.tsx b/packages/features/ee/teams/pages/team-members-view.tsx index 12390b7443..d2871ea882 100644 --- a/packages/features/ee/teams/pages/team-members-view.tsx +++ b/packages/features/ee/teams/pages/team-members-view.tsx @@ -80,7 +80,7 @@ const MembersView = () => { const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(showDialog); const [showInviteLinkSettingsModal, setInviteLinkSettingsModal] = useState(false); const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { - enabled: !!session.data?.user?.organizationId, + enabled: !!session.data?.user?.org, }); const { data: orgMembersNotInThisTeam, isLoading: isOrgListLoading } = diff --git a/packages/features/settings/layouts/SettingsLayout.tsx b/packages/features/settings/layouts/SettingsLayout.tsx index 1971d4e1f7..d032021772 100644 --- a/packages/features/settings/layouts/SettingsLayout.tsx +++ b/packages/features/settings/layouts/SettingsLayout.tsx @@ -159,7 +159,7 @@ const useTabs = () => { // check if name is in adminRequiredKeys return tabs.filter((tab) => { - if (organizationRequiredKeys.includes(tab.name)) return !!session.data?.user?.organizationId; + if (organizationRequiredKeys.includes(tab.name)) return !!session.data?.user?.org; if (isAdmin) return true; return !adminRequiredKeys.includes(tab.name); @@ -205,7 +205,7 @@ const SettingsSidebarContainer = ({ const { data: teams } = trpc.viewer.teams.list.useQuery(); const session = useSession(); const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { - enabled: !!session.data?.user?.organizationId, + enabled: !!session.data?.user?.org, }); const { data: otherTeams } = trpc.viewer.organizations.listOtherTeams.useQuery(); diff --git a/packages/features/shell/Shell.tsx b/packages/features/shell/Shell.tsx index 6a6b353749..004801f69e 100644 --- a/packages/features/shell/Shell.tsx +++ b/packages/features/shell/Shell.tsx @@ -22,7 +22,6 @@ import AdminPasswordBanner from "@calcom/features/users/components/AdminPassword import VerifyEmailBanner from "@calcom/features/users/components/VerifyEmailBanner"; import classNames from "@calcom/lib/classNames"; import { APP_NAME, DESKTOP_APP_LINK, JOIN_DISCORD, ROADMAP, WEBAPP_URL } from "@calcom/lib/constants"; -import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import getBrandColours from "@calcom/lib/getBrandColours"; import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl"; import { useIsomorphicLayoutEffect } from "@calcom/lib/hooks/useIsomorphicLayoutEffect"; @@ -792,13 +791,12 @@ function SideBarContainer({ bannersHeight }: SideBarContainerProps) { function SideBar({ bannersHeight, user }: SideBarProps) { const { t, isLocaleReady } = useLocale(); const orgBranding = useOrgBranding(); - const isOrgBrandingDataFetched = orgBranding !== undefined; const publicPageUrl = useMemo(() => { - if (!user?.organizationId) return `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user?.username}`; + if (!user?.org?.id) return `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user?.username}`; const publicPageUrl = orgBranding?.slug ? getOrgFullDomain(orgBranding.slug) : ""; return publicPageUrl; - }, [orgBranding?.slug, user?.organizationId, user?.username]); + }, [orgBranding?.slug, user?.username, user?.org?.id]); const bottomNavItems: NavigationItemType[] = [ { @@ -819,7 +817,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) { }, { name: "settings", - href: user?.organizationId ? `/settings/organizations/profile` : "/settings/my-account/profile", + href: user?.org ? `/settings/organizations/profile` : "/settings/my-account/profile", icon: Settings, }, ]; @@ -830,12 +828,12 @@ function SideBar({ bannersHeight, user }: SideBarProps) { className="desktop-transparent bg-muted border-muted fixed left-0 hidden h-full max-h-screen w-14 flex-col overflow-y-auto overflow-x-hidden border-r dark:bg-gradient-to-tr dark:from-[#2a2a2a] dark:to-[#1c1c1c] md:sticky md:flex lg:w-56 lg:px-3">
- {!isOrgBrandingDataFetched ? null : orgBranding ? ( + {orgBranding ? (

diff --git a/packages/features/users/components/UserTable/ChangeUserRoleModal.tsx b/packages/features/users/components/UserTable/ChangeUserRoleModal.tsx index d96b5e8094..1f5e51a12e 100644 --- a/packages/features/users/components/UserTable/ChangeUserRoleModal.tsx +++ b/packages/features/users/components/UserTable/ChangeUserRoleModal.tsx @@ -7,7 +7,7 @@ import type { Action, State } from "./UserListTable"; export function ChangeUserRoleModal(props: { state: State; dispatch: Dispatch }) { const { data: session } = useSession(); - const orgId = session?.user.organizationId; + const orgId = session?.user.org?.id; if (!orgId || !props.state.changeMemberRole.user) return null; return ( diff --git a/packages/features/users/components/UserTable/DeleteMemberModal.tsx b/packages/features/users/components/UserTable/DeleteMemberModal.tsx index 2651f90ced..e88bae7ac8 100644 --- a/packages/features/users/components/UserTable/DeleteMemberModal.tsx +++ b/packages/features/users/components/UserTable/DeleteMemberModal.tsx @@ -40,10 +40,10 @@ export function DeleteMemberModal({ state, dispatch }: { state: State; dispatch: confirmBtnText={t("confirm_remove_member")} onConfirm={() => { // Shouldnt ever happen just for type safety - if (!session?.user.organizationId || !state?.deleteMember?.user?.id) return; + if (!session?.user.org?.id || !state?.deleteMember?.user?.id) return; removeMemberMutation.mutate({ - teamId: session?.user.organizationId, + teamId: session?.user.org.id, memberId: state?.deleteMember?.user.id, isOrg: true, }); diff --git a/packages/features/users/components/UserTable/ImpersonationMemberModal.tsx b/packages/features/users/components/UserTable/ImpersonationMemberModal.tsx index e17ae810d8..c5cf7b90d9 100644 --- a/packages/features/users/components/UserTable/ImpersonationMemberModal.tsx +++ b/packages/features/users/components/UserTable/ImpersonationMemberModal.tsx @@ -9,7 +9,7 @@ import type { Action, State } from "./UserListTable"; export function ImpersonationMemberModal(props: { state: State; dispatch: Dispatch }) { const { t } = useLocale(); const { data: session } = useSession(); - const teamId = session?.user.organizationId; + const teamId = session?.user.org?.id; const user = props.state.impersonateMember.user; if (!user || !teamId) return null; diff --git a/packages/features/users/components/UserTable/InviteMemberModal.tsx b/packages/features/users/components/UserTable/InviteMemberModal.tsx index cead1d6775..9f8f16b880 100644 --- a/packages/features/users/components/UserTable/InviteMemberModal.tsx +++ b/packages/features/users/components/UserTable/InviteMemberModal.tsx @@ -46,9 +46,9 @@ export function InviteMemberModal(props: Props) { }, }); - if (!session?.user.organizationId) return null; + if (!session?.user.org?.id) return null; - const orgId = session.user.organizationId; + const orgId = session.user.org.id; return ( Date: Fri, 8 Sep 2023 00:23:57 +0000 Subject: [PATCH 02/11] New Crowdin translations by Github Action --- apps/web/public/static/locales/es/common.json | 2 +- apps/web/public/static/locales/sr/common.json | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/web/public/static/locales/es/common.json b/apps/web/public/static/locales/es/common.json index 178e0b32e4..9200e3495e 100644 --- a/apps/web/public/static/locales/es/common.json +++ b/apps/web/public/static/locales/es/common.json @@ -1888,7 +1888,7 @@ "organization_name": "Nombre de la organización", "organization_url": "URL de la organización", "organization_verify_header": "Verifique el correo electrónico de su organización", - "organization_verify_email_body": "Utilice el código a continuación para verificar su dirección de correo electrónico para seguir configurando su organización.", + "organization_verify_email_body": "Utilice el siguiente código para verificar su dirección de correo electrónico y continuar con la configuración de su organización.", "additional_url_parameters": "Parámetros adicionales de URL", "about_your_organization": "Acerca de su organización", "about_your_organization_description": "Las organizaciones son entornos compartidos donde puede crear varios equipos con miembros, tipos de eventos, aplicaciones, flujos de trabajo compartidos y más.", diff --git a/apps/web/public/static/locales/sr/common.json b/apps/web/public/static/locales/sr/common.json index 47f98b422d..84affde501 100644 --- a/apps/web/public/static/locales/sr/common.json +++ b/apps/web/public/static/locales/sr/common.json @@ -308,7 +308,7 @@ "layout": "Raspored", "bookerlayout_default_title": "Podrazumevani prikaz", "bookerlayout_description": "Možete da izaberete više njih, a vaši učesnici mogu da menjaju prikaze.", - "bookerlayout_user_settings_title": "Raspored zakazivanja", + "bookerlayout_user_settings_title": "Režim prikaza rezervacija", "bookerlayout_user_settings_description": "Možete da izaberete više njih, a učesnici mogu da menjaju prikaz. Ovo se može zameniti za svaki događaj.", "bookerlayout_month_view": "Mesec", "bookerlayout_week_view": "Nedeljno", @@ -404,7 +404,7 @@ "recording_ready": "Link za preuzimanje snimka je spreman", "booking_created": "Rezervacija Napravljena", "booking_rejected": "Rezevacija je odbijena", - "booking_requested": "Rezervacija je zahtevana", + "booking_requested": "Zahtev za rezervaciju je poslat", "meeting_ended": "Sastanak se završio", "form_submitted": "Formular poslat", "event_triggers": "Okidači Dogadjaja", @@ -552,11 +552,11 @@ "team_description": "Par rečenica o vašem timu. Ovo će se pojaviti na stranici URL adrese vašeg tima.", "org_description": "Nekoliko rečenica o vašoj organizaciji. Ovo će se pojaviti na url stranici vaše organizacije.", "members": "Članovi", - "organization_members": "Članovi organizacije", + "organization_members": "Korisnici uključeni u tarifni plan Organization", "member": "Član", "number_member_one": "{{count}} član", "number_member_other": "{{count}} članova", - "number_selected": "{{count}} izabrano", + "number_selected": "Izabrano: {{count}}", "owner": "Vlasnik", "admin": "Admin", "administrator_user": "Administrator", @@ -1687,7 +1687,7 @@ "attendee_no_longer_attending": "Polaznik više ne pohađa vaš događaj", "attendee_no_longer_attending_subtitle": "Korisnik {{name}} je otkazao. To znači da se otvorilo mesto za ovaj vremenski period", "create_event_on": "Kreirajte događaj na", - "create_routing_form_on": "Kreiraj obrazac za usmeravanje", + "create_routing_form_on": "Kreiranje obrasca za usmeravanje za", "default_app_link_title": "Podesite podrazumevani link aplikacije", "default_app_link_description": "Podešavanje podrazumevanog linka aplikacije omogućava novokreiranim tipovima događaja da koriste link aplikacije koji ste postavili.", "organizer_default_conferencing_app": "Podrazumevana aplikacija organizatora", @@ -1876,15 +1876,15 @@ "connect_google_workspace": "Poveži Google Workspace", "google_workspace_admin_tooltip": "Morate da budete Workspace administrator da biste koristili ovu opciju", "first_event_type_webhook_description": "Napravite svoj prvi webhook za ovaj tip događaja", - "install_app_on": "Instaliraj aplikaciju", + "install_app_on": "Instaliraj aplikaciju za", "create_for": "Napravi za", - "organization_banner_description": "Kreirajte okruženje gde vaši timovi mogu da postave deljene aplikacije, radne tokove i vrste događaja sa kružnom dodelom i zajedničko zakazivanje.", + "organization_banner_description": "Kreirajte okruženja gde vaši timovi mogu da postave deljene aplikacije, radne tokove i vrste događaja sa kružnom dodelom i zajedničko zakazivanje.", "organization_banner_title": "Upravljajte organizacijama sa više timova", - "set_up_your_organization": "Postavite svoju organizaciju", + "set_up_your_organization": "Konfigurisanje profila organizacije", "organizations_description": "Organizacije su deljena okruženja gde timovi mogu da kreiraju deljene vrste događaja, aplikacije, radne tokove i još mnogo toga.", "must_enter_organization_name": "Morate da unesete naziv organizacije", "must_enter_organization_admin_email": "Morate da unesete adresu e-pošte vaše organizacije", - "admin_email": "Imejl adresa vaše organizacije", + "admin_email": "Vaša adresa e-pošte u organizaciji", "admin_username": "Korisničko ime administratora", "organization_name": "Naziv organizacije", "organization_url": "URL organizacije", From d8961f8eb9db2bbacf49fe6503e61296a706b2f8 Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Fri, 8 Sep 2023 05:02:12 -0300 Subject: [PATCH 03/11] fix: subteam invitation without teamId associated (#11222) --- .../viewer/teams/inviteMember/utils.ts | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts index e4142789fa..a70b3b95bc 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts @@ -239,19 +239,19 @@ export async function sendVerificationEmail({ }) { const token: string = randomBytes(32).toString("hex"); - if (!connectionInfo.autoAccept) { - await prisma.verificationToken.create({ - data: { - identifier: usernameOrEmail, - token, - expires: new Date(new Date().setHours(168)), // +1 week - team: { - connect: { - id: connectionInfo.orgId || input.teamId, - }, + await prisma.verificationToken.create({ + data: { + identifier: usernameOrEmail, + token, + expires: new Date(new Date().setHours(168)), // +1 week + team: { + connect: { + id: connectionInfo.orgId || input.teamId, }, }, - }); + }, + }); + if (!connectionInfo.autoAccept) { await sendTeamInviteEmail({ language: translation, from: ctx.user.name || `${team.name}'s admin`, @@ -262,15 +262,6 @@ export async function sendVerificationEmail({ isOrg: input.isOrg, }); } else { - // we have already joined the team in createNewUserConnectToOrgIfExists so we dont need to connect via token - await prisma.verificationToken.create({ - data: { - identifier: usernameOrEmail, - token, - expires: new Date(new Date().setHours(168)), // +1 week - }, - }); - await sendOrganizationAutoJoinEmail({ language: translation, from: ctx.user.name || `${team.name}'s admin`, From f25f8e09096f384ec11e1958de9448f49d864c24 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 8 Sep 2023 08:05:45 +0000 Subject: [PATCH 04/11] New Crowdin translations by Github Action --- apps/web/public/static/locales/sr/common.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/public/static/locales/sr/common.json b/apps/web/public/static/locales/sr/common.json index 84affde501..05890ad483 100644 --- a/apps/web/public/static/locales/sr/common.json +++ b/apps/web/public/static/locales/sr/common.json @@ -1888,11 +1888,11 @@ "admin_username": "Korisničko ime administratora", "organization_name": "Naziv organizacije", "organization_url": "URL organizacije", - "organization_verify_header": "Potvrdite imejl vaše organizacije", + "organization_verify_header": "Potvrdite svoju adresu e-pošte u organizaciji", "organization_verify_email_body": "Koristite kôd u nastavku da potvrdite svoju adresu e-pošte kako biste nastavili sa podešavanjem svoje organizacije.", "additional_url_parameters": "Dodatni URL parametri", "about_your_organization": "O vašoj organizaciji", - "about_your_organization_description": "Organizacije su deljena okruženja gde možete da kreirate više timova sa deljenim članovima, vrste događaja, aplikacije, radne tokove i drugo.", + "about_your_organization_description": "Organizacije su deljena okruženja gde možete da kreirate više timova sa deljenim članovima, vrstama događaja, aplikacijama, radnim tokovima i mnogim drugim stvarima.", "create_your_teams": "Kreirajte svoje timove", "create_your_teams_description": "Počnite planiranje zajedno dodavanjem članova tima u svoju organizaciju", "invite_organization_admins": "Pozovite administratore vaše organizacije", @@ -1917,7 +1917,7 @@ "org_no_teams_yet_description": "Ako ste administrator, obavezno kreirajte timove koji će ovde biti prikazani.", "set_up": "Podesi", "set_up_your_profile": "Podesite svoj profil", - "set_up_your_profile_description": "Neka ljudi u organizaciji {{orgName}} znaju ko ste i kako da se povežu sa vama putem javnog linka.", + "set_up_your_profile_description": "Unesite informacije o svojoj ulozi u organizaciji {{orgName}}. Te informacije će videti osobe koje kliknu na vaš javni link.", "my_profile": "Moj profil", "my_settings": "Moja podešavanja", "crm": "CRM", @@ -1925,7 +1925,7 @@ "sender_id_info": "Ime ili broj koji će biti prikazani kao pošiljalac SMS-a (neke zemlje ne dozvoljavaju alfanumeričke ID-ove pošiljaoca)", "org_admins_can_create_new_teams": "Samo administrator vaše ogranizacije može da kreira nove timove", "google_new_spam_policy": "Google-ova nova politika neželjene pošte može da spreči da primate bilo koje imejlove ili obaveštenja kalendara u vezi sa ovom rezervacijom.", - "resolve": "Reši", + "resolve": "Kako rešiti taj problem", "no_organization_slug": "Desila se greška kod kreiranja timova za ovu organizaciju. Nedostaje URL slug.", "org_name": "Naziv organizacije", "org_url": "URL organizacije", @@ -1933,7 +1933,7 @@ "404_the_org": "Organizacija", "404_the_team": "Tim", "404_claim_entity_org": "Zatražite poddomen za svoju organizaciju", - "404_claim_entity_team": "Zahtevajte ovaj tim i počnite da uređujete kolektivni raspored", + "404_claim_entity_team": "Postanite deo ovog tima i počnite da uređujete kolektivni raspored", "insights_all_org_filter": "Sve aplikacije", "insights_team_filter": "Tim: {{teamName}}", "insights_user_filter": "Korisnik: {{userName}}", From 3132c642055cc4335b49225acc0eb7138b33b197 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Fri, 8 Sep 2023 11:19:27 +0100 Subject: [PATCH 05/11] perf: Improve getAggregatedAvailability by reducing reliance on Day.js (#11224) --- packages/core/getAggregatedAvailability.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/core/getAggregatedAvailability.ts b/packages/core/getAggregatedAvailability.ts index 37b32b6970..c40de6be84 100644 --- a/packages/core/getAggregatedAvailability.ts +++ b/packages/core/getAggregatedAvailability.ts @@ -22,25 +22,33 @@ export const getAggregatedAvailability = ( return mergeOverlappingDateRanges(availability); }; +function isSameDay(date1: Date, date2: Date) { + return ( + date1.getUTCFullYear() === date2.getUTCFullYear() && + date1.getUTCMonth() === date2.getUTCMonth() && + date1.getUTCDate() === date2.getUTCDate() + ); +} + function mergeOverlappingDateRanges(dateRanges: DateRange[]) { - const sortedDateRanges = dateRanges.sort((a, b) => a.start.diff(b.start)); //is it already sorted before? + dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf()); const mergedDateRanges: DateRange[] = []; - let currentRange = sortedDateRanges[0]; + let currentRange = dateRanges[0]; if (!currentRange) { return []; } - for (let i = 1; i < sortedDateRanges.length; i++) { - const nextRange = sortedDateRanges[i]; + for (let i = 1; i < dateRanges.length; i++) { + const nextRange = dateRanges[i]; if ( - currentRange.start.utc().format("DD MM YY") === nextRange.start.utc().format("DD MM YY") && - currentRange.end.isAfter(nextRange.start) + isSameDay(currentRange.start.toDate(), nextRange.start.toDate()) && + currentRange.end.valueOf() > nextRange.start.valueOf() ) { currentRange = { start: currentRange.start, - end: currentRange.end.isAfter(nextRange.end) ? currentRange.end : nextRange.end, + end: currentRange.end.valueOf() > nextRange.end.valueOf() ? currentRange.end : nextRange.end, }; } else { mergedDateRanges.push(currentRange); From 4ef52aa2c6789e83614db55dd736c57cf4d99cc6 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Fri, 8 Sep 2023 12:39:29 +0100 Subject: [PATCH 06/11] perf: Move query to get just the user email to the main get query (#11230) --- packages/core/getBusyTimes.ts | 13 +++---------- packages/core/getUserAvailability.ts | 1 + packages/prisma/selects/user.ts | 3 +-- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/core/getBusyTimes.ts b/packages/core/getBusyTimes.ts index 297c64e0d1..b997e2bf8b 100644 --- a/packages/core/getBusyTimes.ts +++ b/packages/core/getBusyTimes.ts @@ -13,6 +13,7 @@ import type { EventBusyDetails } from "@calcom/types/Calendar"; export async function getBusyTimes(params: { credentials: Credential[]; userId: number; + userEmail: string; username: string; eventTypeId?: number; startTime: string; @@ -27,6 +28,7 @@ export async function getBusyTimes(params: { const { credentials, userId, + userEmail, username, eventTypeId, startTime, @@ -45,15 +47,6 @@ export async function getBusyTimes(params: { status: BookingStatus.ACCEPTED, })}` ); - // get user email for attendee checking. - const user = await prisma.user.findUniqueOrThrow({ - where: { - id: userId, - }, - select: { - email: true, - }, - }); /** * A user is considered busy within a given time period if there @@ -97,7 +90,7 @@ export async function getBusyTimes(params: { ...sharedQuery, attendees: { some: { - email: user.email, + email: userEmail, }, }, }, diff --git a/packages/core/getUserAvailability.ts b/packages/core/getUserAvailability.ts index 3617679d9c..9783478654 100644 --- a/packages/core/getUserAvailability.ts +++ b/packages/core/getUserAvailability.ts @@ -180,6 +180,7 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni endTime: getBusyTimesEnd, eventTypeId, userId: user.id, + userEmail: user.email, username: `${user.username}`, beforeEventBuffer, afterEventBuffer, diff --git a/packages/prisma/selects/user.ts b/packages/prisma/selects/user.ts index 3da14c31a0..9c62e70275 100644 --- a/packages/prisma/selects/user.ts +++ b/packages/prisma/selects/user.ts @@ -3,6 +3,7 @@ import { Prisma } from "@prisma/client"; export const availabilityUserSelect = Prisma.validator()({ id: true, timeZone: true, + email: true, bufferTime: true, startTime: true, username: true, @@ -22,7 +23,6 @@ export const availabilityUserSelect = Prisma.validator()({ }); export const baseUserSelect = Prisma.validator()({ - email: true, name: true, destinationCalendar: true, locale: true, @@ -35,7 +35,6 @@ export const baseUserSelect = Prisma.validator()({ export const userSelect = Prisma.validator()({ select: { - email: true, name: true, allowDynamicBooking: true, destinationCalendar: true, From 7e2ad3cea90481634fe824eeb886cb660e3e55bd Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Fri, 8 Sep 2023 16:59:05 +0200 Subject: [PATCH 07/11] fix: inverted location logo (#11233) Co-authored-by: Udit Takkar --- .../components/event-meta/AvailableEventLocations.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/features/bookings/components/event-meta/AvailableEventLocations.tsx b/packages/features/bookings/components/event-meta/AvailableEventLocations.tsx index 34d84f24a4..18f2a2064e 100644 --- a/packages/features/bookings/components/event-meta/AvailableEventLocations.tsx +++ b/packages/features/bookings/components/event-meta/AvailableEventLocations.tsx @@ -21,7 +21,10 @@ function RenderIcon({ return ( {`${eventLocationType.label} ); From 9c3cbee3632933592448f2f742ab2fbcd8907592 Mon Sep 17 00:00:00 2001 From: Shubham Singh Date: Fri, 8 Sep 2023 21:07:26 +0530 Subject: [PATCH 08/11] feat: preference to show/hide available seats count in events (#11109) Co-authored-by: Peer Richelsen --- apps/api/lib/validations/event-type.ts | 3 ++ apps/api/pages/api/bookings/_post.ts | 3 ++ apps/api/pages/api/event-types/[id]/_patch.ts | 3 ++ .../components/eventtype/EventAdvancedTab.tsx | 8 +++ apps/web/pages/booking/[uid].tsx | 2 + apps/web/pages/event-types/[type]/index.tsx | 5 ++ apps/web/public/static/locales/en/common.json | 3 ++ packages/core/EventManager.ts | 1 + packages/features/bookings/Booker/Booker.tsx | 1 + .../Booker/components/AvailableTimeSlots.tsx | 4 ++ .../bookings/Booker/components/EventMeta.tsx | 14 ++--- packages/features/bookings/Booker/store.ts | 2 + .../bookings/components/AvailableTimes.tsx | 14 +++-- .../components/SeatsAvailabilityText.tsx | 51 +++++++++++++++++++ .../features/bookings/lib/handleNewBooking.ts | 2 + packages/features/embed/Embed.tsx | 1 + .../features/eventtypes/lib/getPublicEvent.ts | 1 + packages/lib/defaultEvents.ts | 1 + packages/lib/getEventTypeById.ts | 1 + packages/lib/test/builder.ts | 1 + .../migration.sql | 2 + packages/prisma/schema.prisma | 1 + packages/prisma/zod-utils.ts | 1 + .../deleteCredential.handler.ts | 2 + .../routers/viewer/bookings/get.handler.ts | 1 + packages/types/Calendar.d.ts | 1 + 26 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 packages/features/bookings/components/SeatsAvailabilityText.tsx create mode 100644 packages/prisma/migrations/20230902163155_add_seats_show_availability_count_field/migration.sql diff --git a/apps/api/lib/validations/event-type.ts b/apps/api/lib/validations/event-type.ts index 3c6c839fc1..295884ab46 100644 --- a/apps/api/lib/validations/event-type.ts +++ b/apps/api/lib/validations/event-type.ts @@ -70,6 +70,7 @@ const schemaEventTypeCreateParams = z recurringEvent: recurringEventInputSchema.optional(), seatsPerTimeSlot: z.number().optional(), seatsShowAttendees: z.boolean().optional(), + seatsShowAvailabilityCount: z.boolean().optional(), bookingFields: eventTypeBookingFields.optional(), scheduleId: z.number().optional(), }) @@ -89,6 +90,7 @@ const schemaEventTypeEditParams = z length: z.number().int().optional(), seatsPerTimeSlot: z.number().optional(), seatsShowAttendees: z.boolean().optional(), + seatsShowAvailabilityCount: z.boolean().optional(), bookingFields: eventTypeBookingFields.optional(), scheduleId: z.number().optional(), }) @@ -129,6 +131,7 @@ export const schemaEventTypeReadPublic = EventType.pick({ metadata: true, seatsPerTimeSlot: true, seatsShowAttendees: true, + seatsShowAvailabilityCount: true, bookingFields: true, bookingLimits: true, durationLimits: true, diff --git a/apps/api/pages/api/bookings/_post.ts b/apps/api/pages/api/bookings/_post.ts index 39e9fc69a5..aae99a031b 100644 --- a/apps/api/pages/api/bookings/_post.ts +++ b/apps/api/pages/api/bookings/_post.ts @@ -94,6 +94,9 @@ import { defaultResponder } from "@calcom/lib/server"; * seatsShowAttendees: * type: boolean * description: 'Share Attendee information in seats' + * seatsShowAvailabilityCount: + * type: boolean + * description: 'Show the number of available seats' * smsReminderNumber: * type: number * description: 'SMS reminder number' diff --git a/apps/api/pages/api/event-types/[id]/_patch.ts b/apps/api/pages/api/event-types/[id]/_patch.ts index 19f50db189..f70bd28407 100644 --- a/apps/api/pages/api/event-types/[id]/_patch.ts +++ b/apps/api/pages/api/event-types/[id]/_patch.ts @@ -146,6 +146,9 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission * seatsShowAttendees: * type: boolean * description: 'Share Attendee information in seats' + * seatsShowAvailabilityCount: + * type: boolean + * description: 'Show the number of available seats' * locations: * type: array * description: A list of all available locations for the event type diff --git a/apps/web/components/eventtype/EventAdvancedTab.tsx b/apps/web/components/eventtype/EventAdvancedTab.tsx index 540021f92a..c95d28a4f9 100644 --- a/apps/web/components/eventtype/EventAdvancedTab.tsx +++ b/apps/web/components/eventtype/EventAdvancedTab.tsx @@ -377,6 +377,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick

+
+ formMethods.setValue("seatsShowAvailabilityCount", e.target.checked)} + defaultChecked={!!eventType.seatsShowAvailabilityCount} + /> +
)} /> diff --git a/apps/web/pages/booking/[uid].tsx b/apps/web/pages/booking/[uid].tsx index 0e5072778e..a77dc76965 100644 --- a/apps/web/pages/booking/[uid].tsx +++ b/apps/web/pages/booking/[uid].tsx @@ -917,6 +917,7 @@ const getEventTypesFromDB = async (id: number) => { metadata: true, seatsPerTimeSlot: true, seatsShowAttendees: true, + seatsShowAvailabilityCount: true, periodStartDate: true, periodEndDate: true, }, @@ -940,6 +941,7 @@ const handleSeatsEventTypeOnBooking = async ( eventType: { seatsPerTimeSlot?: number | null; seatsShowAttendees: boolean | null; + seatsShowAvailabilityCount: boolean | null; [x: string | number | symbol]: unknown; }, bookingInfo: Partial< diff --git a/apps/web/pages/event-types/[type]/index.tsx b/apps/web/pages/event-types/[type]/index.tsx index 8b54d23b62..91b3901e0e 100644 --- a/apps/web/pages/event-types/[type]/index.tsx +++ b/apps/web/pages/event-types/[type]/index.tsx @@ -113,6 +113,7 @@ export type FormValues = { periodDates: { startDate: Date; endDate: Date }; seatsPerTimeSlot: number | null; seatsShowAttendees: boolean | null; + seatsShowAvailabilityCount: boolean | null; seatsPerTimeSlotEnabled: boolean; minimumBookingNotice: number; minimumBookingNoticeInDurationType: number; @@ -360,6 +361,7 @@ const EventTypePage = (props: EventTypeSetupProps) => { afterBufferTime, seatsPerTimeSlot, seatsShowAttendees, + seatsShowAvailabilityCount, bookingLimits, durationLimits, recurringEvent, @@ -426,6 +428,7 @@ const EventTypePage = (props: EventTypeSetupProps) => { durationLimits, seatsPerTimeSlot, seatsShowAttendees, + seatsShowAvailabilityCount, metadata, customInputs, children, @@ -460,6 +463,7 @@ const EventTypePage = (props: EventTypeSetupProps) => { afterBufferTime, seatsPerTimeSlot, seatsShowAttendees, + seatsShowAvailabilityCount, bookingLimits, durationLimits, recurringEvent, @@ -516,6 +520,7 @@ const EventTypePage = (props: EventTypeSetupProps) => { durationLimits, seatsPerTimeSlot, seatsShowAttendees, + seatsShowAvailabilityCount, metadata, customInputs, }); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 90bd57c0e2..716c477f69 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -974,6 +974,8 @@ "offer_seats_description": "Offer seats for booking. This automatically disables guest & opt-in bookings.", "seats_available_one": "Seat available", "seats_available_other": "Seats available", + "seats_nearly_full": "Seats almost full", + "seats_half_full": "Seats filling fast", "number_of_seats": "Number of seats per booking", "enter_number_of_seats": "Enter number of seats", "you_can_manage_your_schedules": "You can manage your schedules on the Availability page.", @@ -1455,6 +1457,7 @@ "add_limit": "Add Limit", "team_name_required": "Team name required", "show_attendees": "Share attendee information between guests", + "show_available_seats_count": "Show the number of available seats", "how_booking_questions_as_variables": "How to use booking questions as variables?", "format": "Format", "uppercase_for_letters": "Use uppercase for all letters", diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index a56648bcab..b244278efb 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -262,6 +262,7 @@ export default class EventManager { select: { seatsPerTimeSlot: true, seatsShowAttendees: true, + seatsShowAvailabilityCount: true, }, }, }, diff --git a/packages/features/bookings/Booker/Booker.tsx b/packages/features/bookings/Booker/Booker.tsx index 7459b4d599..fc9d1dde34 100644 --- a/packages/features/bookings/Booker/Booker.tsx +++ b/packages/features/bookings/Booker/Booker.tsx @@ -327,6 +327,7 @@ const BookerComponent = ({ prefetchNextMonth={prefetchNextMonth} monthCount={monthCount} seatsPerTimeSlot={event.data?.seatsPerTimeSlot} + showAvailableSeatsCount={event.data?.seatsShowAvailabilityCount} /> diff --git a/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx b/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx index 8f1e01b773..3243d1e5e2 100644 --- a/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx +++ b/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx @@ -19,6 +19,7 @@ type AvailableTimeSlotsProps = { prefetchNextMonth: boolean; monthCount: number | undefined; seatsPerTimeSlot?: number | null; + showAvailableSeatsCount?: boolean | null; }; /** @@ -32,6 +33,7 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot, + showAvailableSeatsCount, prefetchNextMonth, monthCount, }: AvailableTimeSlotsProps) => { @@ -60,6 +62,7 @@ export const AvailableTimeSlots = ({ seatsPerTimeSlot, attendees, bookingUid, + showAvailableSeatsCount, }); if (seatsPerTimeSlot && seatsPerTimeSlot - attendees > 1) { @@ -116,6 +119,7 @@ export const AvailableTimeSlots = ({ date={dayjs(slots.date)} slots={slots.slots} seatsPerTimeSlot={seatsPerTimeSlot} + showAvailableSeatsCount={showAvailableSeatsCount} availableMonth={ dayjs(selectedDate).format("MM") !== dayjs(slots.date).format("MM") ? dayjs(slots.date).format("MMM") diff --git a/packages/features/bookings/Booker/components/EventMeta.tsx b/packages/features/bookings/Booker/components/EventMeta.tsx index 2f0f4cbb05..92b117c689 100644 --- a/packages/features/bookings/Booker/components/EventMeta.tsx +++ b/packages/features/bookings/Booker/components/EventMeta.tsx @@ -4,6 +4,7 @@ import { shallow } from "zustand/shallow"; import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe"; import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings"; +import { SeatsAvailabilityText } from "@calcom/features/bookings/components/SeatsAvailabilityText"; import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details"; import { useTimePreferences } from "@calcom/features/bookings/lib"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -130,13 +131,12 @@ export const EventMeta = () => {

- {bookingSeatAttendeesQty ? eventTotalSeats - bookingSeatAttendeesQty : eventTotalSeats} /{" "} - {eventTotalSeats}{" "} - {t("seats_available", { - count: bookingSeatAttendeesQty - ? eventTotalSeats - bookingSeatAttendeesQty - : eventTotalSeats, - })} +

diff --git a/packages/features/bookings/Booker/store.ts b/packages/features/bookings/Booker/store.ts index ef5bf203d5..04a995edca 100644 --- a/packages/features/bookings/Booker/store.ts +++ b/packages/features/bookings/Booker/store.ts @@ -33,6 +33,7 @@ type SeatedEventData = { seatsPerTimeSlot?: number | null; attendees?: number; bookingUid?: string; + showAvailableSeatsCount?: boolean | null; }; export type BookerStore = { @@ -206,6 +207,7 @@ export const useBookerStore = create((set, get) => ({ seatsPerTimeSlot: undefined, attendees: undefined, bookingUid: undefined, + showAvailableSeatsCount: true, }, setSeatedEventData: (seatedEventData: SeatedEventData) => { set({ seatedEventData }); diff --git a/packages/features/bookings/components/AvailableTimes.tsx b/packages/features/bookings/components/AvailableTimes.tsx index a97e5fb95d..0808394202 100644 --- a/packages/features/bookings/components/AvailableTimes.tsx +++ b/packages/features/bookings/components/AvailableTimes.tsx @@ -12,6 +12,7 @@ import { Button, SkeletonText } from "@calcom/ui"; import { useBookerStore } from "../Booker/store"; import { useTimePreferences } from "../lib"; +import { SeatsAvailabilityText } from "./SeatsAvailabilityText"; import { TimeFormatToggle } from "./TimeFormatToggle"; type AvailableTimesProps = { @@ -24,6 +25,7 @@ type AvailableTimesProps = { bookingUid?: string ) => void; seatsPerTimeSlot?: number | null; + showAvailableSeatsCount?: boolean | null; showTimeFormatToggle?: boolean; className?: string; availableMonth?: string | undefined; @@ -35,6 +37,7 @@ export const AvailableTimes = ({ slots, onTimeSelect, seatsPerTimeSlot, + showAvailableSeatsCount, showTimeFormatToggle = true, className, availableMonth, @@ -110,15 +113,16 @@ export const AvailableTimes = ({ {dayjs.utc(slot.time).tz(timezone).format(timeFormat)} {bookingFull &&

{t("booking_full")}

} {hasTimeSlots && !bookingFull && ( -

+

- {slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot}{" "} - {t("seats_available", { - count: slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot, - })} +

)} diff --git a/packages/features/bookings/components/SeatsAvailabilityText.tsx b/packages/features/bookings/components/SeatsAvailabilityText.tsx new file mode 100644 index 0000000000..3616c37a88 --- /dev/null +++ b/packages/features/bookings/components/SeatsAvailabilityText.tsx @@ -0,0 +1,51 @@ +import { classNames } from "@calcom/lib"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; + +type Props = { + /** + * Whether to show the exact number of seats available or not + * + * @default true + */ + showExact: boolean; + /** + * Shows available seats count as either whole number or fraction. + * + * Applies only when `showExact` is `true` + * + * @default "whole" + */ + variant?: "whole" | "fraction"; + /** Number of seats booked in the event */ + bookedSeats: number; + /** Total number of seats in the event */ + totalSeats: number; +}; + +export const SeatsAvailabilityText = ({ + showExact = true, + bookedSeats, + totalSeats, + variant = "whole", +}: Props) => { + const { t } = useLocale(); + const availableSeats = totalSeats - bookedSeats; + const isHalfFull = bookedSeats / totalSeats >= 0.5; + const isNearlyFull = bookedSeats / totalSeats >= 0.83; + + return ( + + {showExact + ? `${availableSeats}${variant === "fraction" ? ` / ${totalSeats}` : ""} ${t("seats_available", { + count: availableSeats, + })}` + : isNearlyFull + ? t("seats_nearly_full") + : isHalfFull + ? t("seats_half_full") + : t("seats_available", { + count: availableSeats, + })} + + ); +}; diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index fc344fa536..f7ad9e8ede 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -275,6 +275,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => { seatsPerTimeSlot: true, recurringEvent: true, seatsShowAttendees: true, + seatsShowAvailabilityCount: true, bookingLimits: true, durationLimits: true, parentId: true, @@ -1071,6 +1072,7 @@ async function handler( // if seats are not enabled we should default true seatsShowAttendees: eventType.seatsPerTimeSlot ? eventType.seatsShowAttendees : true, seatsPerTimeSlot: eventType.seatsPerTimeSlot, + seatsShowAvailabilityCount: eventType.seatsPerTimeSlot ? eventType.seatsShowAvailabilityCount : true, schedulingType: eventType.schedulingType, }; diff --git a/packages/features/embed/Embed.tsx b/packages/features/embed/Embed.tsx index 7648bd64e7..d561f951e0 100644 --- a/packages/features/embed/Embed.tsx +++ b/packages/features/embed/Embed.tsx @@ -263,6 +263,7 @@ const EmailEmbed = ({ eventType, username }: { eventType?: EventType; username: } onTimeSelect={onTimeSelect} slots={slots} + showAvailableSeatsCount={eventType.seatsShowAvailabilityCount} /> ) : null} diff --git a/packages/features/eventtypes/lib/getPublicEvent.ts b/packages/features/eventtypes/lib/getPublicEvent.ts index 9132b96fac..ced065b262 100644 --- a/packages/features/eventtypes/lib/getPublicEvent.ts +++ b/packages/features/eventtypes/lib/getPublicEvent.ts @@ -39,6 +39,7 @@ const publicEventSelect = Prisma.validator()({ price: true, currency: true, seatsPerTimeSlot: true, + seatsShowAvailabilityCount: true, bookingFields: true, team: { select: { diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts index c0871fb983..8b0c0cbd8e 100644 --- a/packages/lib/defaultEvents.ts +++ b/packages/lib/defaultEvents.ts @@ -79,6 +79,7 @@ const commons = { schedulingType: SchedulingType.COLLECTIVE, seatsPerTimeSlot: null, seatsShowAttendees: null, + seatsShowAvailabilityCount: null, id: 0, hideCalendarNotes: false, recurringEvent: null, diff --git a/packages/lib/getEventTypeById.ts b/packages/lib/getEventTypeById.ts index 094a236186..592b74643a 100644 --- a/packages/lib/getEventTypeById.ts +++ b/packages/lib/getEventTypeById.ts @@ -174,6 +174,7 @@ export default async function getEventTypeById({ destinationCalendar: true, seatsPerTimeSlot: true, seatsShowAttendees: true, + seatsShowAvailabilityCount: true, webhooks: { select: { id: true, diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index 43c25e4077..550b90175e 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -93,6 +93,7 @@ export const buildEventType = (eventType?: Partial): EventType => { afterEventBuffer: 0, seatsPerTimeSlot: null, seatsShowAttendees: null, + seatsShowAvailabilityCount: null, schedulingType: null, scheduleId: null, bookingLimits: null, diff --git a/packages/prisma/migrations/20230902163155_add_seats_show_availability_count_field/migration.sql b/packages/prisma/migrations/20230902163155_add_seats_show_availability_count_field/migration.sql new file mode 100644 index 0000000000..5f66dd5b82 --- /dev/null +++ b/packages/prisma/migrations/20230902163155_add_seats_show_availability_count_field/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "EventType" ADD COLUMN "seatsShowAvailabilityCount" BOOLEAN DEFAULT true; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index c6bf3208d4..199c2ea2af 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -98,6 +98,7 @@ model EventType { afterEventBuffer Int @default(0) seatsPerTimeSlot Int? seatsShowAttendees Boolean? @default(false) + seatsShowAvailabilityCount Boolean? @default(true) schedulingType SchedulingType? schedule Schedule? @relation(fields: [scheduleId], references: [id]) scheduleId Int? diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index 1b62e2e1d3..fd05ce23bb 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -575,6 +575,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit Date: Fri, 8 Sep 2023 18:29:06 -0300 Subject: [PATCH 09/11] fix: showing profile settings of other teams (#11245) * fix: showing profile settings of other teams * Updating URL --- packages/features/settings/layouts/SettingsLayout.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/features/settings/layouts/SettingsLayout.tsx b/packages/features/settings/layouts/SettingsLayout.tsx index d032021772..aa3fd3a7ee 100644 --- a/packages/features/settings/layouts/SettingsLayout.tsx +++ b/packages/features/settings/layouts/SettingsLayout.tsx @@ -523,6 +523,12 @@ const SettingsSidebarContainer = ({ + Date: Fri, 8 Sep 2023 21:31:37 +0000 Subject: [PATCH 10/11] New Crowdin translations by Github Action --- apps/web/public/static/locales/fr/common.json | 3 +++ apps/web/public/static/locales/hr/common.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index cc253d2d3e..3f3a5eb7ac 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -974,6 +974,8 @@ "offer_seats_description": "Proposez des places de réservation. Cela désactive automatiquement les réservations d'invités et d'opt-in.", "seats_available_one": "Place disponible", "seats_available_other": "Places disponibles", + "seats_nearly_full": "Places presque toutes occupées", + "seats_half_full": "Les places partent vite", "number_of_seats": "Nombre de places par réservation", "enter_number_of_seats": "Saisir le nombre de sièges", "you_can_manage_your_schedules": "Vous pouvez gérer vos disponibilités sur la page Disponibilité.", @@ -1455,6 +1457,7 @@ "add_limit": "Ajouter une limite", "team_name_required": "Nom d'équipe requis", "show_attendees": "Partagez les informations des participants entre les invités", + "show_available_seats_count": "Afficher le nombre de places disponibles", "how_booking_questions_as_variables": "Comment utiliser les questions de réservation comme variables ?", "format": "Format", "uppercase_for_letters": "Utilisez des majuscules pour toutes les lettres.", diff --git a/apps/web/public/static/locales/hr/common.json b/apps/web/public/static/locales/hr/common.json index fd5dec298d..9b998321bc 100644 --- a/apps/web/public/static/locales/hr/common.json +++ b/apps/web/public/static/locales/hr/common.json @@ -210,6 +210,8 @@ "done": "Učinjeno", "all_done": "Završeno!", "all": "Sve", + "available_apps": "Dostupne Aplikacije", + "available_apps_lower_case": "Dostupne aplikacije", "check_email_reset_password": "Provjerite svoju e-poštu. Poslali smo vam link za resetiranje lozinke.", "finish": "Završi", "few_sentences_about_yourself": "Nekoliko rečenica o sebi. Ovo će se pojaviti na vašoj osobnoj stranici.", @@ -331,5 +333,6 @@ "dark": "Tamna", "automatically_adjust_theme": "Automatski prilagodite temu na temelju preferencija pozvanih osoba", "user_dynamic_booking_disabled": "Neki od korisnika u grupi trenutno su onemogućili dinamičke grupne rezervacije", + "full_name": "Puno ime", "insights_all_org_filter": "Sve" } From 37b22977eecd013135343ff6b062e7ab6438e96b Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Sun, 10 Sep 2023 22:30:51 +0530 Subject: [PATCH 11/11] fix: Booking and Cancellation related text changes (#10744) Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> --- apps/web/public/static/locales/en/common.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 716c477f69..2066154d39 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -292,9 +292,9 @@ "add_another_calendar": "Add another calendar", "other": "Other", "email_sign_in_subject": "Your sign-in link for {{appName}}", - "emailed_you_and_attendees": "We emailed you and the other attendees a calendar invitation with all the details.", - "emailed_you_and_attendees_recurring": "We emailed you and the other attendees a calendar invitation for the first of these recurring events.", - "emailed_you_and_any_other_attendees": "You and any other attendees have been emailed with this information.", + "emailed_you_and_attendees": "We sent an email with a calendar invitation with the details to everyone.", + "emailed_you_and_attendees_recurring": "We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.", + "emailed_you_and_any_other_attendees": "We sent an email to everyone with this information.", "needs_to_be_confirmed_or_rejected": "Your booking still needs to be confirmed or rejected.", "needs_to_be_confirmed_or_rejected_recurring": "Your recurring meeting still needs to be confirmed or rejected.", "user_needs_to_confirm_or_reject_booking": "{{user}} still needs to confirm or reject the booking.", @@ -1102,7 +1102,7 @@ "reschedule_optional": "Reason for rescheduling (optional)", "reschedule_placeholder": "Let others know why you need to reschedule", "event_cancelled": "This event is canceled", - "emailed_information_about_cancelled_event": "We emailed you and the other attendees to let them know.", + "emailed_information_about_cancelled_event": "We sent an email to everyone to let them know.", "this_input_will_shown_booking_this_event": "This input will be shown when booking this event", "meeting_url_in_confirmation_email": "Meeting url is in the confirmation email", "url_start_with_https": "URL needs to start with http:// or https://",