diff --git a/apps/web/components/eventtype/EventSetupTab.tsx b/apps/web/components/eventtype/EventSetupTab.tsx index 2683fb764b..07a7d7d3b3 100644 --- a/apps/web/components/eventtype/EventSetupTab.tsx +++ b/apps/web/components/eventtype/EventSetupTab.tsx @@ -13,7 +13,6 @@ import type { EventLocationType } from "@calcom/app-store/locations"; import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations"; import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; -import cx from "@calcom/lib/classNames"; import { CAL_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { md } from "@calcom/lib/markdownIt"; @@ -302,13 +301,7 @@ export const EventSetupTab = (
{`${eventLocationType.label} {`${eventLabel} ${ diff --git a/apps/web/components/getting-started/steps-views/UserProfile.tsx b/apps/web/components/getting-started/steps-views/UserProfile.tsx index aae5e10f7d..f197ff4461 100644 --- a/apps/web/components/getting-started/steps-views/UserProfile.tsx +++ b/apps/web/components/getting-started/steps-views/UserProfile.tsx @@ -3,12 +3,13 @@ import type { FormEvent } from "react"; import { useRef, useState } from "react"; import { useForm } from "react-hook-form"; +import OrganizationAvatar from "@calcom/features/ee/organizations/components/OrganizationAvatar"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { md } from "@calcom/lib/markdownIt"; import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import turndown from "@calcom/lib/turndownService"; import { trpc } from "@calcom/trpc/react"; -import { Avatar, Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui"; +import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui"; import { ArrowRight } from "@calcom/ui/components/icon"; type FormData = { @@ -98,7 +99,14 @@ const UserProfile = () => { return (
- {user && } + {user && ( + + )} ; const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => { return (
- {icon && ( - cover - )} + {icon && cover} {label}
); diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 1858a06b46..be0fe69cdd 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -254,6 +254,10 @@ const nextConfig = { source: "/org/:slug", destination: "/team/:slug", }, + { + source: "/org/:orgSlug/avatar.png", + destination: "/api/user/avatar?orgSlug=:orgSlug", + }, { source: "/team/:teamname/avatar.png", destination: "/api/user/avatar?teamname=:teamname", diff --git a/apps/web/package.json b/apps/web/package.json index e210ce672f..9d13ac0e26 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/web", - "version": "3.2.8", + "version": "3.2.9", "private": true, "scripts": { "analyze": "ANALYZE=true next build", diff --git a/apps/web/pages/[user].tsx b/apps/web/pages/[user].tsx index e94a0b1557..5afe8b1154 100644 --- a/apps/web/pages/[user].tsx +++ b/apps/web/pages/[user].tsx @@ -11,6 +11,7 @@ import { useEmbedStyles, useIsEmbed, } from "@calcom/embed-core/embed-iframe"; +import OrganizationAvatar from "@calcom/features/ee/organizations/components/OrganizationAvatar"; import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains"; import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components"; @@ -25,7 +26,7 @@ import prisma from "@calcom/prisma"; import type { EventType, User } from "@calcom/prisma/client"; import { baseEventTypeSelect } from "@calcom/prisma/selects"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; -import { Avatar, HeadSeo, UnpublishedEntity } from "@calcom/ui"; +import { HeadSeo, UnpublishedEntity } from "@calcom/ui"; import { Verified, ArrowRight } from "@calcom/ui/components/icon"; import type { EmbedProps } from "@lib/withEmbedSsr"; @@ -96,7 +97,12 @@ export function UserPage(props: InferGetServerSidePropsType
- +

{profile.name} {user.verified && ( @@ -218,6 +224,7 @@ export type UserPageProps = { theme: string | null; brandColor: string; darkBrandColor: string; + organizationSlug: string | null; allowSEOIndexing: boolean; }; users: Pick[]; @@ -321,6 +328,7 @@ export const getServerSideProps: GetServerSideProps = async (cont theme: user.theme, brandColor: user.brandColor, darkBrandColor: user.darkBrandColor, + organizationSlug: user.organization?.slug ?? null, allowSEOIndexing: user.allowSEOIndexing ?? true, }; diff --git a/apps/web/pages/api/user/avatar.ts b/apps/web/pages/api/user/avatar.ts index edb6fb24b2..43cd00f0e1 100644 --- a/apps/web/pages/api/user/avatar.ts +++ b/apps/web/pages/api/user/avatar.ts @@ -10,6 +10,7 @@ const querySchema = z .object({ username: z.string(), teamname: z.string(), + orgSlug: z.string(), /** * Allow fetching avatar of a particular organization * Avatars being public, we need not worry about others accessing it. @@ -19,7 +20,7 @@ const querySchema = z .partial(); async function getIdentityData(req: NextApiRequest) { - const { username, teamname, orgId } = querySchema.parse(req.query); + const { username, teamname, orgId, orgSlug } = querySchema.parse(req.query); const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.host ?? ""); const org = isValidOrgDomain ? currentOrgDomain : null; @@ -59,7 +60,23 @@ async function getIdentityData(req: NextApiRequest) { org, name: teamname, email: null, - avatar: team?.logo || getPlaceholderAvatar(null, teamname), + avatar: getPlaceholderAvatar(team?.logo, teamname), + }; + } + if (orgSlug) { + const org = await prisma.team.findFirst({ + where: getSlugOrRequestedSlug(orgSlug), + select: { + slug: true, + logo: true, + name: true, + }, + }); + return { + org: org?.slug, + name: org?.name, + email: null, + avatar: getPlaceholderAvatar(org?.logo, org?.name), }; } } diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index 2ec6ddd4d9..738c642e72 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -781,6 +781,12 @@ const CreateFirstEventTypeView = () => { Icon={LinkIcon} headline={t("new_event_type_heading")} description={t("new_event_type_description")} + className="mb-16" + buttonRaw={ + + } /> ); }; diff --git a/apps/web/pages/settings/my-account/profile.tsx b/apps/web/pages/settings/my-account/profile.tsx index c83fe55106..4f36f5a933 100644 --- a/apps/web/pages/settings/my-account/profile.tsx +++ b/apps/web/pages/settings/my-account/profile.tsx @@ -6,6 +6,7 @@ import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; +import OrganizationAvatar from "@calcom/features/ee/organizations/components/OrganizationAvatar"; import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -14,10 +15,10 @@ import turndown from "@calcom/lib/turndownService"; import { IdentityProvider } from "@calcom/prisma/enums"; import type { TRPCClientErrorLike } from "@calcom/trpc/client"; import { trpc } from "@calcom/trpc/react"; +import type { RouterOutputs } from "@calcom/trpc/react"; import type { AppRouter } from "@calcom/trpc/server/routers/_app"; import { Alert, - Avatar, Button, Dialog, DialogClose, @@ -223,6 +224,7 @@ const ProfileView = () => { key={JSON.stringify(defaultValues)} defaultValues={defaultValues} isLoading={updateProfileMutation.isLoading} + userOrganization={user.organization} onSubmit={(values) => { if (values.email !== user.email && isCALIdentityProvider) { setTempFormValues(values); @@ -364,11 +366,13 @@ const ProfileForm = ({ onSubmit, extraField, isLoading = false, + userOrganization, }: { defaultValues: FormValues; onSubmit: (values: FormValues) => void; extraField?: React.ReactNode; isLoading: boolean; + userOrganization: RouterOutputs["viewer"]["me"]["organization"]; }) => { const { t } = useLocale(); const [firstRender, setFirstRender] = useState(true); @@ -406,7 +410,12 @@ const ProfileForm = ({ name="avatar" render={({ field: { value } }) => ( <> - +
users.deleteAll()); -async function createUserWithSeatedEvent(users: Fixtures["users"]) { - const slug = "seats"; - const user = await users.create({ - eventTypes: [ - { - title: "Seated event", - slug, - seatsPerTimeSlot: 10, - requiresConfirmation: true, - length: 30, - disableGuests: true, // should always be true for seated events - }, - ], - }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const eventType = user.eventTypes.find((e) => e.slug === slug)!; - return { user, eventType }; -} - -async function createUserWithSeatedEventAndAttendees( - fixtures: Pick, - attendees: Prisma.AttendeeCreateManyBookingInput[] -) { - const { user, eventType } = await createUserWithSeatedEvent(fixtures.users); - const booking = await fixtures.bookings.create(user.id, user.username, eventType.id, { - status: BookingStatus.ACCEPTED, - // startTime with 1 day from now and endTime half hour after - startTime: new Date(Date.now() + 24 * 60 * 60 * 1000), - endTime: new Date(Date.now() + 24 * 60 * 60 * 1000 + 30 * 60 * 1000), - attendees: { - createMany: { - data: attendees, - }, - }, - }); - return { user, eventType, booking }; -} - test.describe("Booking with Seats", () => { test("User can create a seated event (2 seats as example)", async ({ users, page }) => { const user = await users.create({ name: "Seated event" }); diff --git a/apps/web/playwright/lib/testUtils.ts b/apps/web/playwright/lib/testUtils.ts index d23493e659..7279d39d8f 100644 --- a/apps/web/playwright/lib/testUtils.ts +++ b/apps/web/playwright/lib/testUtils.ts @@ -6,6 +6,10 @@ import { createServer } from "http"; import { noop } from "lodash"; import type { API, Messages } from "mailhog"; +import type { Prisma } from "@calcom/prisma/client"; +import { BookingStatus } from "@calcom/prisma/enums"; + +import type { Fixtures } from "./fixtures"; import { test } from "./fixtures"; export function todo(title: string) { @@ -192,6 +196,7 @@ export async function installAppleCalendar(page: Page) { await page.waitForURL("/apps/apple-calendar"); await page.click('[data-testid="install-app-button"]'); } + export async function getEmailsReceivedByUser({ emails, userEmail, @@ -228,3 +233,44 @@ export async function expectEmailsToHaveSubject({ expect(organizerFirstEmail.subject).toBe(emailSubject); expect(bookerFirstEmail.subject).toBe(emailSubject); } + +// this method is not used anywhere else +// but I'm keeping it here in case we need in the future +async function createUserWithSeatedEvent(users: Fixtures["users"]) { + const slug = "seats"; + const user = await users.create({ + eventTypes: [ + { + title: "Seated event", + slug, + seatsPerTimeSlot: 10, + requiresConfirmation: true, + length: 30, + disableGuests: true, // should always be true for seated events + }, + ], + }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const eventType = user.eventTypes.find((e) => e.slug === slug)!; + return { user, eventType }; +} + +export async function createUserWithSeatedEventAndAttendees( + fixtures: Pick, + attendees: Prisma.AttendeeCreateManyBookingInput[] +) { + const { user, eventType } = await createUserWithSeatedEvent(fixtures.users); + + const booking = await fixtures.bookings.create(user.id, user.username, eventType.id, { + status: BookingStatus.ACCEPTED, + // startTime with 1 day from now and endTime half hour after + startTime: new Date(Date.now() + 24 * 60 * 60 * 1000), + endTime: new Date(Date.now() + 24 * 60 * 60 * 1000 + 30 * 60 * 1000), + attendees: { + createMany: { + data: attendees, + }, + }, + }); + return { user, eventType, booking }; +} diff --git a/apps/web/playwright/webhook.e2e.ts b/apps/web/playwright/webhook.e2e.ts index af0be0f580..d5e1d5b512 100644 --- a/apps/web/playwright/webhook.e2e.ts +++ b/apps/web/playwright/webhook.e2e.ts @@ -1,5 +1,9 @@ import type { Page } from "@playwright/test"; import { expect } from "@playwright/test"; +import { v4 as uuidv4 } from "uuid"; + +import prisma from "@calcom/prisma"; +import { BookingStatus } from "@calcom/prisma/client"; import { test } from "./lib/fixtures"; import { @@ -8,6 +12,7 @@ import { selectFirstAvailableTimeSlotNextMonth, waitFor, gotoRoutingLink, + createUserWithSeatedEventAndAttendees, } from "./lib/testUtils"; // remove dynamic properties that differs depending on where you run the tests @@ -15,6 +20,29 @@ const dynamic = "[redacted/dynamic]"; test.afterEach(({ users }) => users.deleteAll()); +async function createWebhookReceiver(page: Page) { + const webhookReceiver = createHttpServer(); + + await page.goto(`/settings/developer/webhooks`); + + // --- add webhook + await page.click('[data-testid="new_webhook"]'); + + await page.fill('[name="subscriberUrl"]', webhookReceiver.url); + + await page.fill('[name="secret"]', "secret"); + + await Promise.all([ + page.click("[type=submit]"), + page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")), + ]); + + // page contains the url + expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined(); + + return webhookReceiver; +} + test.describe("BOOKING_CREATED", async () => { test("add webhook & test that creating an event triggers a webhook call", async ({ page, @@ -388,6 +416,147 @@ test.describe("BOOKING_REQUESTED", async () => { }); }); +test.describe("BOOKING_RESCHEDULED", async () => { + test("can reschedule a booking and get a booking rescheduled event", async ({ page, users, bookings }) => { + const user = await users.create(); + const [eventType] = user.eventTypes; + + await user.apiLogin(); + + const webhookReceiver = await createWebhookReceiver(page); + + const booking = await bookings.create(user.id, user.username, eventType.id, { + status: BookingStatus.ACCEPTED, + }); + + await page.goto(`/${user.username}/${eventType.slug}?rescheduleUid=${booking.uid}`); + + await selectFirstAvailableTimeSlotNextMonth(page); + + await page.locator('[data-testid="confirm-reschedule-button"]').click(); + + await expect(page).toHaveURL(/.*booking/); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } })!; + expect(newBooking).not.toBeNull(); + + // --- check that webhook was called + await waitFor(() => { + expect(webhookReceiver.requestList.length).toBe(1); + }); + + const [request] = webhookReceiver.requestList; + + expect(request.body).toMatchObject({ + triggerEvent: "BOOKING_RESCHEDULED", + payload: { + uid: newBooking?.uid, + }, + }); + }); + + test("when rescheduling to a booking that already exists, should send a booking rescheduled event with the existant booking uid", async ({ + page, + users, + bookings, + }) => { + const { user, eventType, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [ + { name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" }, + { name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" }, + ]); + + await prisma.eventType.update({ + where: { id: eventType.id }, + data: { requiresConfirmation: false }, + }); + + await user.apiLogin(); + + const webhookReceiver = await createWebhookReceiver(page); + + const bookingAttendees = await prisma.attendee.findMany({ + where: { bookingId: booking.id }, + select: { + id: true, + email: true, + }, + }); + + const bookingSeats = bookingAttendees.map((attendee) => ({ + bookingId: booking.id, + attendeeId: attendee.id, + referenceUid: uuidv4(), + })); + + await prisma.bookingSeat.createMany({ + data: bookingSeats, + }); + + const references = await prisma.bookingSeat.findMany({ + where: { bookingId: booking.id }, + include: { attendee: true }, + }); + + await page.goto(`/reschedule/${references[0].referenceUid}`); + + await selectFirstAvailableTimeSlotNextMonth(page); + + await page.locator('[data-testid="confirm-reschedule-button"]').click(); + + await expect(page).toHaveURL(/.*booking/); + + const newBooking = await prisma.booking.findFirst({ + where: { + attendees: { + some: { + email: bookingAttendees[0].email, + }, + }, + }, + }); + + // --- ensuring that new booking was created + expect(newBooking).not.toBeNull(); + + // --- check that webhook was called + await waitFor(() => { + expect(webhookReceiver.requestList.length).toBe(1); + }); + + const [firstRequest] = webhookReceiver.requestList; + + expect(firstRequest?.body).toMatchObject({ + triggerEvent: "BOOKING_RESCHEDULED", + payload: { + uid: newBooking?.uid, + }, + }); + + await page.goto(`/reschedule/${references[1].referenceUid}`); + + await selectFirstAvailableTimeSlotNextMonth(page); + + await page.locator('[data-testid="confirm-reschedule-button"]').click(); + + await expect(page).toHaveURL(/.*booking/); + + await waitFor(() => { + expect(webhookReceiver.requestList.length).toBe(2); + }); + + const [_, secondRequest] = webhookReceiver.requestList; + + expect(secondRequest?.body).toMatchObject({ + triggerEvent: "BOOKING_RESCHEDULED", + payload: { + // in the current implementation, it is the same as the first booking + uid: newBooking?.uid, + }, + }); + }); +}); + test.describe("FORM_SUBMITTED", async () => { test("on submitting user form, triggers user webhook", async ({ page, users, routingForms }) => { const webhookReceiver = createHttpServer(); diff --git a/apps/web/public/static/locales/ar/common.json b/apps/web/public/static/locales/ar/common.json index 230f09817d..1b969ad897 100644 --- a/apps/web/public/static/locales/ar/common.json +++ b/apps/web/public/static/locales/ar/common.json @@ -1078,7 +1078,7 @@ "url_start_with_https": "يجب أن يبدأ العنوان بـ http:// أو https://", "number_provided": "سيتم توفير رقم الهاتف", "before_event_trigger": "قبل بدء الحدث", - "event_cancelled_trigger": "متى تم إلغاء هذا الحدث", + "event_cancelled_trigger": "عند إلغاء هذا الحدث", "new_event_trigger": "متى تم حجز الحدث الجديد", "email_host_action": "إرسال رسالة إلكترونية إلى المضيف", "email_attendee_action": "إرسال رسالة إلكترونية إلى الحضور", @@ -1833,7 +1833,7 @@ "invite_link_copied": "تم نسخ رابط الدعوة", "invite_link_deleted": "تم حذف رابط الدعوة", "invite_link_updated": "تم حفظ إعدادات رابط الدعوة", - "link_expires_after": "تم تعيين الروابط للانتهاء بعد...", + "link_expires_after": "تم تعيين انتهاء صلاحية الروابط بعد...", "one_day": "1 يوم", "seven_days": "7 أيام", "thirty_days": "30 يومًا", @@ -1884,7 +1884,7 @@ "organization_verify_email_body": "الرجاء استخدام الرمز أدناه لتأكيد عنوان بريدك الإلكتروني لمواصلة إعداد منظمتك.", "additional_url_parameters": "معلمات الرابط الإضافية", "about_your_organization": "حول منظمتك", - "about_your_organization_description": "المنظمات هي بيئات مشتركة حيث يمكنك إنشاء فرق متعددة مع أعضاء مشتركين وأنواع الأحداث والتطبيقات ومهام سير العمل والمزيد.", + "about_your_organization_description": "المنظمات هي بيئات مشتركة حيث يمكنك إنشاء فرق متعددة مع أعضاء مشتركين وأنواع الأحداث والتطبيقات ومهام سير العمل المشتركة والمزيد.", "create_your_teams": "إنشاء فرقك", "create_your_teams_description": "ابدأوا الجدولة معًا عن طريق إضافة أعضاء فريقك إلى منظمتك", "invite_organization_admins": "دعوة مشرفي منظمتك", @@ -1925,7 +1925,7 @@ "404_the_org": "المنظمة", "404_the_team": "الفريق", "404_claim_entity_org": "المطالبة بنطاقك الفرعي لمنظمتك", - "404_claim_entity_team": "المطالبة بهذا الفريق والبدء في إدارة الجداول الزمنية بشكل جماعي", + "404_claim_entity_team": "انضم لهذا الفريق وابدأ في إدارة الجداول الزمنية بشكل جماعي", "insights_all_org_filter": "الكل", "insights_team_filter": "الفريق: {{teamName}}", "insights_user_filter": "المستخدم: {{userName}}", diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index a10b48fef4..90bd57c0e2 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2045,5 +2045,6 @@ "recently_added":"Recently added", "no_members_found": "No members found", "event_setup_length_error":"Event Setup: The duration must be at least 1 minute.", + "availability_schedules":"Availability Schedules", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/es/common.json b/apps/web/public/static/locales/es/common.json index 62ca4d6420..178e0b32e4 100644 --- a/apps/web/public/static/locales/es/common.json +++ b/apps/web/public/static/locales/es/common.json @@ -129,7 +129,7 @@ "team_upgrade_banner_description": "Gracias por probar nuestro nuevo plan de equipo. Notamos que su equipo \"{{teamName}}\" necesita actualizarse.", "upgrade_banner_action": "Actualizar aquí", "team_upgraded_successfully": "¡Tu equipo se actualizó con éxito!", - "org_upgrade_banner_description": "Gracias por probar nuestro nuevo plan de equipo. Notamos que su equipo \"{{teamName}}\" necesita actualizarse.", + "org_upgrade_banner_description": "Gracias por probar nuestro nuevo plan Organization. Notamos que su Organization \"{{teamName}}\" necesita actualizarse.", "org_upgraded_successfully": "Su Organization se actualizó con éxito.", "use_link_to_reset_password": "Utilice el enlace de abajo para restablecer su contraseña", "hey_there": "Hola,", @@ -306,7 +306,7 @@ "password_has_been_reset_login": "Su contraseña ha sido restablecida. Ahora puede iniciar sesión con su nueva contraseña.", "layout": "Diseño", "bookerlayout_default_title": "Vista predeterminada", - "bookerlayout_description": "Puede seleccionar varios y quienes le reserven pueden cambiar de vista.", + "bookerlayout_description": "Puede seleccionar varias y quienes le reserven pueden cambiar de vista.", "bookerlayout_user_settings_title": "Diseño de reserva", "bookerlayout_user_settings_description": "Puede seleccionar varios y quienes le reservan pueden cambiar de vista. Esto se puede anular por evento.", "bookerlayout_month_view": "Mes", @@ -315,7 +315,7 @@ "bookerlayout_error_min_one_enabled": "Al menos un diseño tiene que estar habilitado.", "bookerlayout_error_default_not_enabled": "El diseño que seleccionó como vista predeterminada no es parte de los diseños habilitados.", "bookerlayout_error_unknown_layout": "El diseño seleccionado no es un diseño válido.", - "bookerlayout_override_global_settings": "Puede gestionar esto para todos sus tipos de eventos en <2>configuración / apariencia o <6>sobrescribir solo para este evento.", + "bookerlayout_override_global_settings": "Puede gestionar esto para todos sus tipos de eventos en <2>configuración / apariencia o <6>anular solo para este evento.", "unexpected_error_try_again": "Ocurrió un error inesperado. Inténtelo de nuevo.", "sunday_time_error": "Hora inválida del domingo", "monday_time_error": "Hora inválida del lunes", @@ -551,7 +551,7 @@ "team_description": "Comentarios sobre tu equipo. Esta información aparecerá en la página de la URL de tu equipo.", "org_description": "Algunas frases sobre su organización. Esto aparecerá en la página de la URL de su organización.", "members": "Miembros", - "organization_members": "Miembros de la organización", + "organization_members": "Miembros de Organization", "member": "Miembro", "number_member_one": "{{count}} miembro", "number_member_other": "{{count}} miembros", @@ -699,7 +699,7 @@ "create_team_to_get_started": "Crea un equipo para empezar", "teams": "Equipos", "team": "Equipo", - "organization": "Organización", + "organization": "Organization", "team_billing": "Facturación del equipo", "team_billing_description": "Gestione la facturación para su equipo", "upgrade_to_flexible_pro_title": "Hemos cambiado la facturación de los equipos", @@ -1916,7 +1916,7 @@ "org_no_teams_yet_description": "Si usted es un administrador, asegúrese de crear equipos para que se muestren aquí.", "set_up": "Configurar", "set_up_your_profile": "Configure su perfil", - "set_up_your_profile_description": "Informe a las personas quién es usted dentro de {{orgName}} cuándo interactúan con su enlace público.", + "set_up_your_profile_description": "Informe a las personas quién es usted dentro de {{orgName}} y cuándo interactúen con su enlace público.", "my_profile": "Mi perfil", "my_settings": "Mi configuración", "crm": "CRM", diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index 7714f2eb44..cc253d2d3e 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -1060,7 +1060,7 @@ "your_unique_api_key": "Votre clé API unique", "copy_safe_api_key": "Copiez cette clé API et conservez-la dans un endroit sûr. Si vous perdez cette clé, vous devrez en générer une nouvelle.", "zapier_setup_instructions": "<0>Connectez-vous à votre compte Zapier et créez un nouveau Zap.<1>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.<2>Choisissez votre compte, puis saisissez votre clé API unique.<3>Testez votre déclencheur.<4>Vous êtes prêt !", - "make_setup_instructions": "<0>Connectez-vous à votre compte Make et créez un nouveau Scénario.<1>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.<2>Choisissez votre compte, puis saisissez votre clé API unique.<3>Testez votre déclencheur.<4>Vous êtes prêt !", + "make_setup_instructions": "<0>Accédez au <1><0>lien d'invitation Make et installez l'application Cal.com.<1>Connectez-vous à votre compte Make et créez un nouveau scénario.<2>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.<3>Choisissez votre compte, puis saisissez votre clé API unique.<4>Testez votre déclencheur.<5>Vous êtes prêt !", "install_zapier_app": "Veuillez d'abord installer l'application Zapier dans l'App Store.", "install_make_app": "Veuillez d'abord installer l'application Make dans l'App Store.", "connect_apple_server": "Se connecter au serveur d'Apple", @@ -1688,8 +1688,11 @@ "delete_sso_configuration_confirmation_description": "Voulez-vous vraiment supprimer la configuration {{connectionType}} ? Les membres de votre équipe utilisant la connexion {{connectionType}} ne pourront plus accéder à Cal.com.", "organizer_timezone": "Fuseau horaire de l'organisateur", "email_user_cta": "Voir l'invitation", + "email_no_user_invite_heading_team": "Vous avez été invité à rejoindre une équipe {{appName}}", + "email_no_user_invite_heading_org": "Vous avez été invité à rejoindre une organisation {{appName}}", "email_no_user_invite_subheading": "{{invitedBy}} vous a invité à rejoindre son équipe sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre équipe d'organiser des rendez-vous sans échanges d'e-mails.", "email_user_invite_subheading_team": "{{invitedBy}} vous a invité à rejoindre son équipe « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre équipe d'organiser des rendez-vous sans échanges d'e-mails.", + "email_user_invite_subheading_org": "{{invitedBy}} vous a invité à rejoindre son organisation « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre organisation d'organiser des rendez-vous sans échanges d'e-mails.", "email_no_user_invite_steps_intro": "Nous vous guiderons à travers quelques étapes courtes et vous profiterez d'une planification sans stress avec votre {{entity}} en un rien de temps.", "email_no_user_step_one": "Choisissez votre nom d'utilisateur", "email_no_user_step_two": "Connectez votre compte de calendrier", @@ -2041,5 +2044,7 @@ "include_calendar_event": "Inclure l'événement du calendrier", "recently_added": "Ajouté récemment", "no_members_found": "Aucun membre trouvé", + "event_setup_length_error": "Configuration de l'événement : la durée doit être d'au moins 1 minute.", + "availability_schedules": "Horaires de disponibilité", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/ja/common.json b/apps/web/public/static/locales/ja/common.json index ce3b4d8048..2f679c5133 100644 --- a/apps/web/public/static/locales/ja/common.json +++ b/apps/web/public/static/locales/ja/common.json @@ -306,7 +306,7 @@ "password_has_been_reset_login": "パスワードがリセットされました。新しく作成したパスワードでログインできるようになりました。", "layout": "レイアウト", "bookerlayout_default_title": "デフォルトの表示", - "bookerlayout_description": "複数のものを選択することができ、予約者は表示を切り替えることができます。", + "bookerlayout_description": "複数のビューを選択することができ、予約者は表示を切り替えることができます。", "bookerlayout_user_settings_title": "予約のレイアウト", "bookerlayout_user_settings_description": "複数のものを選択することができ、予約者は表示を切り替えることができます。これはイベントごとに上書きが可能です。", "bookerlayout_month_view": "月", @@ -391,7 +391,7 @@ "user_dynamic_booking_disabled": "グループ内の一部のユーザーは、現在動的なグループ予約を無効にしています", "allow_dynamic_booking_tooltip": "\"+\" を使って複数のユーザー名を追加することで動的に作成できるグループ予約リンク。例: \"{{appName}}/bailey+peer\"", "allow_dynamic_booking": "出席者が動的なグループ予約を通じてあなたを予約できるようにする", - "dynamic_booking": "動的なグループリンク", + "dynamic_booking": "ダイナミックグループリンク", "email": "Eメールアドレス", "email_placeholder": "jdoe@example.com", "full_name": "フルネーム", @@ -555,7 +555,7 @@ "member": "メンバー", "number_member_one": "{{count}} 人のメンバー", "number_member_other": "{{count}} 人のメンバー", - "number_selected": "{{count}} 件が選択されました", + "number_selected": "{{count}} が選択されました", "owner": "所有者", "admin": "管理者", "administrator_user": "管理者ユーザー", @@ -1150,8 +1150,8 @@ "choose_template": "テンプレートを選択する", "custom": "カスタム", "reminder": "リマインダー", - "rescheduled": "スケジュール変更済み", - "completed": "完了", + "rescheduled": "スケジュールが変更されました", + "completed": "完了しました", "reminder_email": "リマインダー:{{date}} の {{name}} との {{eventType}}", "not_triggering_existing_bookings": "イベントの予約時に電話番号の入力を求められるため、すでにある予約はトリガーされません。", "minute_one": "{{count}} 分", @@ -1672,7 +1672,7 @@ "scheduler": "{Scheduler}", "no_workflows": "ワークフローがありません", "change_filter": "個人やチームのワークフローを表示するためのフィルターを変更します。", - "change_filter_common": "結果を表示するフィルターを変更します。", + "change_filter_common": "フィルターを変更して結果を表示します。", "no_results_for_filter": "このフィルターに該当する結果はありません", "recommended_next_steps": "推奨される次のステップ", "create_a_managed_event": "管理されたイベントの種類を作成", @@ -1696,7 +1696,7 @@ "booking_questions_title": "予約の質問", "booking_questions_description": "予約ページで尋ねる質問をカスタマイズする", "add_a_booking_question": "質問を追加", - "identifier": "識別子", + "identifier": "ID", "duplicate_email": "メールが重複しています", "booking_with_payment_cancelled": "このイベントの支払いはもうできません", "booking_with_payment_cancelled_already_paid": "この予約に関するお支払いの払い戻しについては、現在処理中です。", @@ -1840,7 +1840,7 @@ "invite_link_copied": "招待リンクをコピーしました", "invite_link_deleted": "招待リンクを削除しました", "invite_link_updated": "招待リンクの設定を保存しました", - "link_expires_after": "リンクが期限切れとなるまで、あと...", + "link_expires_after": "リンクの期限切れまで...", "one_day": "1 日", "seven_days": "7 日", "thirty_days": "30 日", @@ -1936,7 +1936,7 @@ "insights_all_org_filter": "すべて", "insights_team_filter": "チーム: {{teamName}}", "insights_user_filter": "ユーザー: {{userName}}", - "insights_subtitle": "イベント全体での予約に関する分析情報を表示する", + "insights_subtitle": "イベント全体での予約に関する Insights を表示する", "custom_plan": "カスタムプラン", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/pt/common.json b/apps/web/public/static/locales/pt/common.json index 7ee5e2676c..5b2df8a52b 100644 --- a/apps/web/public/static/locales/pt/common.json +++ b/apps/web/public/static/locales/pt/common.json @@ -12,7 +12,7 @@ "have_any_questions": "Tem perguntas? Estamos disponíveis para ajudar.", "reset_password_subject": "{{appName}}: Instruções de redefinição da senha", "verify_email_subject": "{{appName}}: Verifique a sua conta", - "check_your_email": "Confirme o seu e-mail", + "check_your_email": "Verifique o seu e-mail", "verify_email_page_body": "Enviámos um e-mail para {{email}}. É importante verificar o seu endereço de e-mail para garantir que receberá as comunicações de {{appName}}.", "verify_email_banner_body": "Confirme o seu endereço de e-mail para garantir a melhor entrega possível de e-mail e de agenda", "verify_email_email_header": "Confirme o seu endereço de e-mail", diff --git a/apps/web/public/static/locales/ru/common.json b/apps/web/public/static/locales/ru/common.json index ffdad17293..23c9cd7d33 100644 --- a/apps/web/public/static/locales/ru/common.json +++ b/apps/web/public/static/locales/ru/common.json @@ -118,7 +118,7 @@ "team_info": "Информация о команде", "request_another_invitation_email": "Если вы не хотите использовать {{toEmail}} как ваш {{appName}} адрес электронной почты или уже есть аккаунт {{appName}}, пожалуйста, запросите другое приглашение на это письмо.", "you_have_been_invited": "Вас пригласили присоединиться к команде {{teamName}}", - "user_invited_you": "{{user}} пригласил вас в команду {{entity}} {{team}} в {{appName}}", + "user_invited_you": "{{user}} пригласил вас в команду {{team}} {{entity}} в {{appName}}", "hidden_team_member_title": "В этой команде вы скрытый пользователь", "hidden_team_member_message": "Ваше место не оплачено. Перейдите на аккаунт PRO или свяжитесь с руководителем команды, чтобы сообщить ему, что он может оплатить ваше место.", "hidden_team_owner_message": "Чтобы работать с командами, необходим аккаунт Pro. До перехода на этот тариф Вы остаетесь скрытым пользователем.", @@ -403,7 +403,7 @@ "recording_ready": "Ссылка для скачивания записи готова", "booking_created": "Бронирование создано", "booking_rejected": "Бронирование отклонено", - "booking_requested": "Поступил запрос на бронирование", + "booking_requested": "Запрос на бронирование отправлен", "meeting_ended": "Встреча завершилась", "form_submitted": "Форма отправлена", "event_triggers": "Триггеры событий", @@ -1879,7 +1879,7 @@ "create_for": "Создать для", "organization_banner_description": "Создавайте рабочие среды, в рамках которых ваши команды смогут создавать общие приложения, рабочие процессы и типы событий с назначением участников по очереди и коллективным планированием.", "organization_banner_title": "Управляйте организациями с несколькими командами", - "set_up_your_organization": "Настройте организацию", + "set_up_your_organization": "Настройка профиля организации", "organizations_description": "Организация — это общая рабочая среда, в которой команды могут создавать общие типы событий, приложения, рабочие процессы и многое другое.", "must_enter_organization_name": "Необходимо ввести название организации", "must_enter_organization_admin_email": "Необходимо ввести ваш адрес электронной почты в организации", @@ -1887,7 +1887,7 @@ "admin_username": "Имя пользователя администратора", "organization_name": "Название организации", "organization_url": "URL-адрес организации", - "organization_verify_header": "Подтвердите адрес электронной почты вашей организации", + "organization_verify_header": "Подтвердите свой адрес электронной почты в организации", "organization_verify_email_body": "С помощью кода ниже подтвердите свой адрес электронной почты, чтобы продолжить настройку организации.", "additional_url_parameters": "Дополнительные параметры URL-адреса", "about_your_organization": "О вашей организации", diff --git a/apps/web/public/static/locales/sr/common.json b/apps/web/public/static/locales/sr/common.json index a6ad7d83ff..47f98b422d 100644 --- a/apps/web/public/static/locales/sr/common.json +++ b/apps/web/public/static/locales/sr/common.json @@ -130,7 +130,7 @@ "team_upgrade_banner_description": "Hvala vam što isprobavate naš novi plan za timove. Primetili smo da vaš tim „{{teamName}}“ treba da se nadogradi.", "upgrade_banner_action": "Nadogradite ovde", "team_upgraded_successfully": "Vaš tim je uspešno pretplaćen!", - "org_upgrade_banner_description": "Hvala što isprobavate plan naše organizacije. Primetili smo da tim vaše organizacije „{{teamName}}” treba da se nadogradi.", + "org_upgrade_banner_description": "Hvala što isprobavate naš Organization plan. Primetili smo da vaš Organization „{{teamName}}” treba da se nadogradi.", "org_upgraded_successfully": "Vaš Organization je uspešno nadograđen!", "use_link_to_reset_password": "Resetujte lozinku koristeći link ispod", "hey_there": "Zdravo,", diff --git a/apps/web/styles/globals.css b/apps/web/styles/globals.css index 4e09978ccd..f00e4bfe3d 100644 --- a/apps/web/styles/globals.css +++ b/apps/web/styles/globals.css @@ -93,6 +93,12 @@ --cal-brand-text: black; } +@layer base { + * { + @apply border-default + } +} + ::-moz-selection { color: var(--cal-brand-text); background: var(--cal-brand); diff --git a/packages/app-store/routing-forms/emails/components/ResponseEmail.tsx b/packages/app-store/routing-forms/emails/components/ResponseEmail.tsx index 7fde2e765c..1724429435 100644 --- a/packages/app-store/routing-forms/emails/components/ResponseEmail.tsx +++ b/packages/app-store/routing-forms/emails/components/ResponseEmail.tsx @@ -3,15 +3,15 @@ import type { App_RoutingForms_Form } from "@prisma/client"; import { BaseEmailHtml, Info } from "@calcom/emails/src/components"; import { WEBAPP_URL } from "@calcom/lib/constants"; -import type { Response } from "../../types/types"; +import type { OrderedResponses } from "../../types/types"; export const ResponseEmail = ({ form, - response, + orderedResponses, ...props }: { form: Pick; - response: Response; + orderedResponses: OrderedResponses; subject: string; } & Partial>) => { return ( @@ -36,11 +36,11 @@ export const ResponseEmail = ({ title={form.name} subtitle="New Response Received" {...props}> - {Object.entries(response).map(([fieldId, fieldResponse]) => { + {orderedResponses.map((fieldResponse, index) => { return ( ; export default class ResponseEmail extends BaseEmail { - response: Response; + orderedResponses: OrderedResponses; toAddresses: string[]; form: Form; - constructor({ toAddresses, response, form }: { form: Form; toAddresses: string[]; response: Response }) { + constructor({ + toAddresses, + orderedResponses, + form, + }: { + form: Form; + toAddresses: string[]; + orderedResponses: OrderedResponses; + }) { super(); this.form = form; - this.response = response; + this.orderedResponses = orderedResponses; this.toAddresses = toAddresses; } @@ -26,7 +34,7 @@ export default class ResponseEmail extends BaseEmail { subject, html: renderEmail("ResponseEmail", { form: this.form, - response: this.response, + orderedResponses: this.orderedResponses, subject, }), }; diff --git a/packages/app-store/routing-forms/trpc/utils.ts b/packages/app-store/routing-forms/trpc/utils.ts index 4aebde0c7f..c1c926ba72 100644 --- a/packages/app-store/routing-forms/trpc/utils.ts +++ b/packages/app-store/routing-forms/trpc/utils.ts @@ -6,6 +6,7 @@ import logger from "@calcom/lib/logger"; import { WebhookTriggerEvents } from "@calcom/prisma/client"; import type { Ensure } from "@calcom/types/utils"; +import type { OrderedResponses } from "../types/types"; import type { Response, SerializableForm } from "../types/types"; export async function onFormSubmission( @@ -61,23 +62,28 @@ export async function onFormSubmission( }); await Promise.all(promises); + const orderedResponses = form.fields.reduce((acc, field) => { + acc.push(response[field.id]); + return acc; + }, [] as OrderedResponses); + if (form.settings?.emailOwnerOnSubmission) { logger.debug( `Preparing to send Form Response email for Form:${form.id} to form owner: ${form.user.email}` ); - await sendResponseEmail(form, response, form.user.email); + await sendResponseEmail(form, orderedResponses, form.user.email); } } export const sendResponseEmail = async ( form: Pick, - response: Response, + orderedResponses: OrderedResponses, ownerEmail: string ) => { try { if (typeof window === "undefined") { const { default: ResponseEmail } = await import("../emails/templates/response-email"); - const email = new ResponseEmail({ form: form, toAddresses: [ownerEmail], response: response }); + const email = new ResponseEmail({ form: form, toAddresses: [ownerEmail], orderedResponses }); await email.sendEmail(); } } catch (e) { diff --git a/packages/app-store/routing-forms/types/types.d.ts b/packages/app-store/routing-forms/types/types.d.ts index d093ac88fa..7cc639c121 100644 --- a/packages/app-store/routing-forms/types/types.d.ts +++ b/packages/app-store/routing-forms/types/types.d.ts @@ -45,3 +45,5 @@ export type SerializableRoute = isFallback?: LocalRoute["isFallback"]; }) | GlobalRoute; + +export type OrderedResponses = Response[string][]; diff --git a/packages/core/event.ts b/packages/core/event.ts index c410526c80..191dbe1ee3 100644 --- a/packages/core/event.ts +++ b/packages/core/event.ts @@ -57,8 +57,12 @@ export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView if (variable === bookingField) { let fieldValue; if (eventNameObj.bookingFields) { - fieldValue = - eventNameObj.bookingFields[bookingField as keyof typeof eventNameObj.bookingFields]?.toString(); + const field = eventNameObj.bookingFields[bookingField as keyof typeof eventNameObj.bookingFields]; + if (field && typeof field === "object" && "value" in field) { + fieldValue = field?.value?.toString(); + } else { + fieldValue = field?.toString(); + } } dynamicEventName = dynamicEventName.replace(`{${variable}}`, fieldValue || ""); } diff --git a/packages/emails/src/templates/SlugReplacementEmail.tsx b/packages/emails/src/templates/SlugReplacementEmail.tsx index 4623e72b1f..8a889456ef 100644 --- a/packages/emails/src/templates/SlugReplacementEmail.tsx +++ b/packages/emails/src/templates/SlugReplacementEmail.tsx @@ -33,7 +33,7 @@ export const SlugReplacementEmail = (

- Your link will continue to work but somesettings for it may have changed. You can review it in + Your link will continue to work but some settings for it may have changed. You can review it in event types.

diff --git a/packages/features/bookings/components/event-meta/AvailableEventLocations.tsx b/packages/features/bookings/components/event-meta/AvailableEventLocations.tsx index ea558419e0..34d84f24a4 100644 --- a/packages/features/bookings/components/event-meta/AvailableEventLocations.tsx +++ b/packages/features/bookings/components/event-meta/AvailableEventLocations.tsx @@ -21,11 +21,7 @@ function RenderIcon({ return ( {`${eventLocationType.label} ); diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 0dbd8f4021..fc344fa536 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1756,6 +1756,7 @@ async function handler( const webhookData = { ...evt, ...eventTypeInfo, + uid: resultBooking?.uid || uid, bookingId: booking?.id, rescheduleUid, rescheduleStartTime: originalRescheduledBooking?.startTime diff --git a/packages/features/calendars/DatePicker.tsx b/packages/features/calendars/DatePicker.tsx index b206b36ee6..cf69e33ad4 100644 --- a/packages/features/calendars/DatePicker.tsx +++ b/packages/features/calendars/DatePicker.tsx @@ -1,3 +1,4 @@ +import { useEffect } from "react"; import { shallow } from "zustand/shallow"; import type { Dayjs } from "@calcom/dayjs"; @@ -100,40 +101,6 @@ const NoAvailabilityOverlay = ({ ); }; -/** - * Takes care of selecting a valid date in the month if the selected date is not available in the month - */ -const useHandleInitialDateSelection = ({ - daysToRenderForTheMonth, - selected, - onChange, -}: { - daysToRenderForTheMonth: { day: Dayjs | null; disabled: boolean }[]; - selected: Dayjs | Dayjs[] | null | undefined; - onChange: (date: Dayjs | null) => void; -}) => { - // Let's not do something for now in case of multiple selected dates as behaviour is unclear and it's not needed at the moment - if (selected instanceof Array) { - return; - } - const firstAvailableDateOfTheMonth = daysToRenderForTheMonth.find((day) => !day.disabled)?.day; - - const isSelectedDateAvailable = selected - ? daysToRenderForTheMonth.some(({ day, disabled }) => { - if (day && yyyymmdd(day) === yyyymmdd(selected) && !disabled) return true; - }) - : false; - - if (!isSelectedDateAvailable && firstAvailableDateOfTheMonth) { - // If selected date not available in the month, select the first available date of the month - onChange(firstAvailableDateOfTheMonth); - } - - if (!firstAvailableDateOfTheMonth) { - onChange(null); - } -}; - const Days = ({ minDate = dayjs.utc(), excludedDates = [], @@ -218,11 +185,34 @@ const Days = ({ }; }); - useHandleInitialDateSelection({ - daysToRenderForTheMonth, - selected, - onChange: props.onChange, - }); + /** + * Takes care of selecting a valid date in the month if the selected date is not available in the month + */ + + const useHandleInitialDateSelection = () => { + // Let's not do something for now in case of multiple selected dates as behaviour is unclear and it's not needed at the moment + if (selected instanceof Array) { + return; + } + const firstAvailableDateOfTheMonth = daysToRenderForTheMonth.find((day) => !day.disabled)?.day; + + const isSelectedDateAvailable = selected + ? daysToRenderForTheMonth.some(({ day, disabled }) => { + if (day && yyyymmdd(day) === yyyymmdd(selected) && !disabled) return true; + }) + : false; + + if (!isSelectedDateAvailable && firstAvailableDateOfTheMonth) { + // If selected date not available in the month, select the first available date of the month + props.onChange(firstAvailableDateOfTheMonth); + } + + if (!firstAvailableDateOfTheMonth) { + props.onChange(null); + } + }; + + useEffect(useHandleInitialDateSelection); return ( <> diff --git a/packages/features/ee/organizations/components/OrganizationAvatar.tsx b/packages/features/ee/organizations/components/OrganizationAvatar.tsx new file mode 100644 index 0000000000..bf645fefc2 --- /dev/null +++ b/packages/features/ee/organizations/components/OrganizationAvatar.tsx @@ -0,0 +1,31 @@ +import classNames from "@calcom/lib/classNames"; +import { Avatar } from "@calcom/ui"; +import type { AvatarProps } from "@calcom/ui"; + +type OrganizationAvatarProps = AvatarProps & { + organizationSlug: string | null | undefined; +}; + +const OrganizationAvatar = ({ size, imageSrc, alt, organizationSlug, ...rest }: OrganizationAvatarProps) => { + return ( + + {alt} +
+ ) : null + } + /> + ); +}; + +export default OrganizationAvatar; diff --git a/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx b/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx index d1f8e9a77a..2471548564 100644 --- a/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx +++ b/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx @@ -103,7 +103,7 @@ export function AvailabilitySliderTable() { .padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; return ( -
+
{time} GMT {offsetFormatted}
diff --git a/packages/features/users/components/UserTable/EditSheet/EditUserSheet.tsx b/packages/features/users/components/UserTable/EditSheet/EditUserSheet.tsx index 898c884008..c0ab958b19 100644 --- a/packages/features/users/components/UserTable/EditSheet/EditUserSheet.tsx +++ b/packages/features/users/components/UserTable/EditSheet/EditUserSheet.tsx @@ -5,7 +5,7 @@ import { useOrgBranding } from "@calcom/ee/organizations/context/provider"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; -import { Sheet, SheetContent, SheetFooter, Avatar, Skeleton, Loader } from "@calcom/ui"; +import { Sheet, SheetContent, SheetFooter, Avatar, Skeleton, Loader, Label } from "@calcom/ui"; import type { State, Action } from "../UserListTable"; import { DisplayInfo } from "./DisplayInfo"; @@ -69,14 +69,23 @@ export function EditUserSheet({ state, dispatch }: { state: State; dispatch: Dis /> - +
+ +
+ {schedulesNames + ? schedulesNames.map((scheduleName) => ( + + {scheduleName} + + )) + : t("user_has_no_schedules")} +
+
+ { const { user } = ctx; - const { metadata: metadataFromInput } = input; - const cleanMetadata = cleanMetadataAllowedUpdateKeys(metadataFromInput); + const userMetadata = handleUserMetadata({ ctx, input }); const data: Prisma.UserUpdateInput = { ...input, - metadata: cleanMetadata, + metadata: userMetadata, }; // some actions can invalidate a user session. @@ -65,26 +64,9 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) data.avatar = await resizeBase64Image(input.avatar); } - const fetchUserCurrentMetadata = await prisma.user.findUnique({ - where: { - id: user.id, - }, - select: { - metadata: true, - }, - }); - - const metadata = userMetadata.parse(fetchUserCurrentMetadata?.metadata); - - // Required so we don't override and delete saved values - data.metadata = { - ...metadata, - cleanMetadata, - }; - - const isPremium = metadata?.isPremium; if (isPremiumUsername) { - const stripeCustomerId = metadata?.stripeCustomerId; + const stripeCustomerId = userMetadata?.stripeCustomerId; + const isPremium = userMetadata?.isPremium; if (!isPremium || !stripeCustomerId) { throw new TRPCError({ code: "BAD_REQUEST", message: "User is not premium" }); } @@ -199,12 +181,21 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) const cleanMetadataAllowedUpdateKeys = (metadata: TUpdateProfileInputSchema["metadata"]) => { if (!metadata) { - return {} as Prisma.InputJsonValue; + return {}; } const cleanedMetadata = updateUserMetadataAllowedKeys.safeParse(metadata); if (!cleanedMetadata.success) { logger.error("Error cleaning metadata", cleanedMetadata.error); + return {}; } - return cleanedMetadata as Prisma.InputJsonValue; + return cleanedMetadata.data; +}; + +const handleUserMetadata = ({ ctx, input }: UpdateProfileOptions) => { + const { user } = ctx; + const cleanMetadata = cleanMetadataAllowedUpdateKeys(input.metadata); + const userMetadata = userMetadataSchema.parse(user.metadata); + // Required so we don't override and delete saved values + return { ...userMetadata, ...cleanMetadata }; }; diff --git a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts index d68b8f0826..b36aa4886d 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts @@ -275,8 +275,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) => const bookerUrl = await getBookerUrl(user); return { - // don't display event teams without event types, - eventTypeGroups: eventTypeGroups.filter((groupBy) => groupBy.parentId || !!groupBy.eventTypes?.length), + eventTypeGroups, // so we can show a dropdown when the user has teams profiles: eventTypeGroups.map((group) => ({ ...group.profile, diff --git a/packages/ui/components/avatar/Avatar.tsx b/packages/ui/components/avatar/Avatar.tsx index 2c3b3c4c6c..cec1a9f268 100644 --- a/packages/ui/components/avatar/Avatar.tsx +++ b/packages/ui/components/avatar/Avatar.tsx @@ -7,7 +7,6 @@ import { AVATAR_FALLBACK } from "@calcom/lib/constants"; import type { Maybe } from "@trpc/server"; -import { Check } from "../icon"; import { Tooltip } from "../tooltip"; export type AvatarProps = { @@ -20,6 +19,7 @@ export type AvatarProps = { fallback?: React.ReactNode; accepted?: boolean; asChild?: boolean; // Added to ignore the outer span on the fallback component - messes up styling + indicator?: React.ReactNode; }; const sizesPropsBySize = { @@ -34,12 +34,13 @@ const sizesPropsBySize = { } as const; export function Avatar(props: AvatarProps) { - const { imageSrc, size = "md", alt, title, href } = props; + const { imageSrc, size = "md", alt, title, href, indicator } = props; const rootClass = classNames("aspect-square rounded-full", sizesPropsBySize[size]); let avatar = ( @@ -57,17 +58,7 @@ export function Avatar(props: AvatarProps) { {props.fallback ? props.fallback : {alt}} - {props.accepted && ( -
-
- {size === "lg" && } -
-
- )} + {indicator}
); diff --git a/packages/ui/components/avatar/AvatarGroup.tsx b/packages/ui/components/avatar/AvatarGroup.tsx index 8f94a31f38..ca8cb95606 100644 --- a/packages/ui/components/avatar/AvatarGroup.tsx +++ b/packages/ui/components/avatar/AvatarGroup.tsx @@ -11,7 +11,6 @@ export type AvatarGroupProps = { href?: string; }[]; className?: string; - accepted?: boolean; truncateAfter?: number; }; @@ -36,7 +35,6 @@ export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) { imageSrc={item.image} title={item.title} alt={item.alt || ""} - accepted={props.accepted} size={props.size} href={item.href} /> diff --git a/packages/ui/components/command/index.tsx b/packages/ui/components/command/index.tsx index 5761f9fb38..0d7a4d2503 100644 --- a/packages/ui/components/command/index.tsx +++ b/packages/ui/components/command/index.tsx @@ -108,7 +108,7 @@ const CommandItem = React.forwardRef< )}
-

{headline}

+

+ {headline} +

{description && (
{description} diff --git a/packages/ui/components/table/TableNew.tsx b/packages/ui/components/table/TableNew.tsx index 2429a7e51a..7b29c46c5c 100644 --- a/packages/ui/components/table/TableNew.tsx +++ b/packages/ui/components/table/TableNew.tsx @@ -36,10 +36,7 @@ const TableRow = React.forwardRef ( )