- {user &&
}
+ {user && (
+
+ )}
;
const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
return (
- {icon && (
-
![cover]({icon})
- )}
+ {icon &&
![cover]({icon})
}
{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 / apariencia2> o <6>sobrescribir solo para este evento6>.",
+ "bookerlayout_override_global_settings": "Puede gestionar esto para todos sus tipos de eventos en <2>configuración / apariencia2> o <6>anular solo para este evento6>.",
"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.0><1>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.1><2>Choisissez votre compte, puis saisissez votre clé API unique.2><3>Testez votre déclencheur.3><4>Vous êtes prêt !4>",
- "make_setup_instructions": "<0>Connectez-vous à votre compte Make et créez un nouveau Scénario.0><1>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.1><2>Choisissez votre compte, puis saisissez votre clé API unique.2><3>Testez votre déclencheur.3><4>Vous êtes prêt !4>",
+ "make_setup_instructions": "<0>Accédez au <1><0>lien d'invitation Make0>1> et installez l'application Cal.com.0><1>Connectez-vous à votre compte Make et créez un nouveau scénario.1><2>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.2><3>Choisissez votre compte, puis saisissez votre clé API unique.3><4>Testez votre déclencheur.4><5>Vous êtes prêt !5>",
"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 (
);
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 (
+
+
+
+ ) : 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 :
}
>
- {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 (
)