Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
87954a0
Copy changes
pumfleet Jan 8, 2026
b099cb2
Move search bar inline with new button
pumfleet Jan 8, 2026
89edf29
Get rid of no more results message
pumfleet Jan 8, 2026
54a4129
Change hidden badge to (hidden)
pumfleet Jan 8, 2026
c2372d6
Remove Cal.ai badge from sidebar
pumfleet Jan 8, 2026
f003cf1
Add dropdown to create button when there is multiple options
pumfleet Jan 9, 2026
a8312c0
Fix delete dialog
pumfleet Jan 9, 2026
4d4cbc1
Saved filters updates
pumfleet Jan 9, 2026
220a64a
More string fixes
pumfleet Jan 9, 2026
632e614
Switch members table to use names
pumfleet Jan 9, 2026
2cfe3e4
Fix member spacing
pumfleet Jan 9, 2026
03db1bf
Fix routing form identifier field
pumfleet Jan 9, 2026
c7323e7
Fix routing forms stuff
pumfleet Jan 9, 2026
6074e82
Only show SMS hint on SMS options
pumfleet Jan 9, 2026
63d0d15
Make workflow delete button minimal
pumfleet Jan 9, 2026
a04432d
Fix padding on workflow steps
pumfleet Jan 9, 2026
6494fc7
Remove min width on workflow title
pumfleet Jan 9, 2026
086f118
Fix delete workflow PR
pumfleet Jan 9, 2026
ae09084
Fix org profile buttons
pumfleet Jan 9, 2026
aa6303c
Fix org profile screen partially scrolled down
pumfleet Jan 9, 2026
7ad66e8
Improve logos & banner uploads
pumfleet Jan 9, 2026
f4e9951
Personal profile fixes
pumfleet Jan 9, 2026
217d092
Fix settings general view stuff
pumfleet Jan 9, 2026
cb3442d
Sentence case consistency
pumfleet Jan 9, 2026
d4426f2
Merge branch 'main' into ux
pumfleet Jan 9, 2026
e1ce807
Fix stuff I broke
pumfleet Jan 9, 2026
d03f318
Fix fab
pumfleet Jan 12, 2026
9bf3d22
Fix hidden translation string
pumfleet Jan 12, 2026
7d535b7
Fix text fields
pumfleet Jan 12, 2026
78305c7
Make button small for solo users too
pumfleet Jan 13, 2026
2f1845e
Merge branch 'main' into ux
pumfleet Jan 13, 2026
f181940
Merge branch 'main' into ux
pedroccastro Jan 13, 2026
961e211
fix: update E2E tests to match sentence case labels in routing forms
pedroccastro Jan 13, 2026
d5df06d
fix: update tests to match sentence case label changes
pedroccastro Jan 13, 2026
4d5ef11
fix: address Cubic AI review feedback (confidence 9+)
devin-ai-integration[bot] Jan 13, 2026
b0f3ec3
fix: update E2E tests for sentence case label changes
pedroccastro Jan 13, 2026
a1bc518
fix: replace text locator with data-testid in manage-booking-question…
devin-ai-integration[bot] Jan 13, 2026
34de07d
fix: use .last() for multiple location select items
pedroccastro Jan 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import { useDebounce } from "@calcom/lib/hooks/useDebounce";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
import type { ReactElement } from "react";
import { useState } from "react";

import EventTypes, { EventTypesCTA, SearchContext } from "~/event-types/views/event-types-listing-view";

type GetUserEventGroupsResponse = Parameters<typeof EventTypesCTA>[0]["userEventGroupsData"];

const CTAWithContext = ({
userEventGroupsData,
}: {
userEventGroupsData: GetUserEventGroupsResponse;
}): ReactElement => {
return <EventTypesCTA userEventGroupsData={userEventGroupsData} />;
};

export function EventTypesWrapper({
userEventGroupsData,
user,
}: {
userEventGroupsData: GetUserEventGroupsResponse;
user: {
id: number;
completedOnboarding?: boolean;
} | null;
}): ReactElement {
const { t } = useLocale();
const [searchTerm, setSearchTerm] = useState("");
const debouncedSearchTerm = useDebounce(searchTerm, 500);

return (
<SearchContext.Provider value={{ searchTerm, setSearchTerm, debouncedSearchTerm }}>
<ShellMainAppDir
heading={t("event_types_page_title")}
subtitle={t("event_types_page_subtitle")}
CTA={<CTAWithContext userEventGroupsData={userEventGroupsData} />}>
<EventTypes userEventGroupsData={userEventGroupsData} user={user} />
</ShellMainAppDir>
</SearchContext.Provider>
);
}
113 changes: 65 additions & 48 deletions apps/web/app/(use-page-wrapper)/(main-nav)/event-types/page.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,60 @@
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
import { createRouterCaller, getTRPCContext } from "app/_trpc/context";
import type { PageProps, ReadonlyHeaders, ReadonlyRequestCookies } from "app/_types";
import { _generateMetadata, getTranslate } from "app/_utils";
import { unstable_cache } from "next/cache";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";

import { checkOnboardingRedirect } from "@calcom/features/auth/lib/onboardingUtils";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { checkOnboardingRedirect } from "@calcom/features/auth/lib/onboardingUtils";
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
import type { RouterOutputs } from "@calcom/trpc/react";
import { eventTypesRouter } from "@calcom/trpc/server/routers/viewer/eventTypes/_router";

import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { createRouterCaller, getTRPCContext } from "app/_trpc/context";
import type {
PageProps,
ReadonlyHeaders,
ReadonlyRequestCookies,
} from "app/_types";
import { _generateMetadata } from "app/_utils";
import { unstable_cache } from "next/cache";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import type { ReactElement } from "react";

import EventTypes, { EventTypesCTA } from "~/event-types/views/event-types-listing-view";
import { EventTypesWrapper } from "./EventTypesWrapper";

export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("event_types_page_title"),
(t) => t("event_types_page_subtitle"),
undefined,
undefined,
"/event-types"
const getCachedEventGroups: (
headers: ReadonlyHeaders,
cookies: ReadonlyRequestCookies,
filters?: {
teamIds?: number[] | undefined;
userIds?: number[] | undefined;
upIds?: string[] | undefined;
}
) => Promise<RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]> =
unstable_cache(
async (
headers: ReadonlyHeaders,
cookies: ReadonlyRequestCookies,
filters?: {
teamIds?: number[] | undefined;
userIds?: number[] | undefined;
upIds?: string[] | undefined;
}
): Promise<RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]> => {
const eventTypesCaller = await createRouterCaller(
eventTypesRouter,
await getTRPCContext(headers, cookies)
);
return await eventTypesCaller.getUserEventGroups({ filters });
},
["viewer.eventTypes.getUserEventGroups"],
{ revalidate: 3600 } // seconds
);

const getCachedEventGroups = unstable_cache(
async (
headers: ReadonlyHeaders,
cookies: ReadonlyRequestCookies,
filters?: {
teamIds?: number[] | undefined;
userIds?: number[] | undefined;
upIds?: string[] | undefined;
}
) => {
const eventTypesCaller = await createRouterCaller(
eventTypesRouter,
await getTRPCContext(headers, cookies)
);
return await eventTypesCaller.getUserEventGroups({ filters });
},
["viewer.eventTypes.getUserEventGroups"],
{ revalidate: 3600 } // seconds
);

const Page = async ({ searchParams }: PageProps) => {
const Page = async ({ searchParams }: PageProps): Promise<ReactElement> => {
const _searchParams = await searchParams;
const _headers = await headers();
const _cookies = await cookies();

const session = await getServerSession({ req: buildLegacyRequest(_headers, _cookies) });
const session = await getServerSession({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would have to update the skeleton loader on this page as we moved the search input to the top

Screen.Recording.2026-01-10.at.6.24.10.PM.mov

req: buildLegacyRequest(_headers, _cookies),
});
if (!session?.user?.id) {
return redirect("/auth/login");
}
Expand All @@ -65,18 +70,30 @@ const Page = async ({ searchParams }: PageProps) => {
return redirect(onboardingPath);
}

const t = await getTranslate();
const filters = getTeamsFiltersFromQuery(_searchParams);
const userEventGroupsData = await getCachedEventGroups(_headers, _cookies, filters);
const userEventGroupsData = await getCachedEventGroups(
_headers,
_cookies,
filters
);

return (
<ShellMainAppDir
heading={t("event_types_page_title")}
subtitle={t("event_types_page_subtitle")}
CTA={<EventTypesCTA userEventGroupsData={userEventGroupsData} />}>
<EventTypes userEventGroupsData={userEventGroupsData} user={session.user} />
</ShellMainAppDir>
<EventTypesWrapper
userEventGroupsData={userEventGroupsData}
user={session.user}
/>
);
};

export const generateMetadata = async (): Promise<
ReturnType<typeof _generateMetadata>
> =>
await _generateMetadata(
(t) => t("event_types_page_title"),
(t) => t("event_types_page_subtitle"),
undefined,
undefined,
"/event-types"
);

export default Page;
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,16 @@ function Field({
moveUp={moveUp}
moveDown={moveDown}
badge={
router ? { text: router.name, variant: "gray", href: `${appUrl}/form-edit/${router.id}` } : null
router
? {
text: router.name,
variant: "gray",
href: `${appUrl}/form-edit/${router.id}`,
}
: null
}
deleteField={router ? null : deleteField}>
deleteField={router ? null : deleteField}
>
<FormCardBody>
<div className="mb-3 w-full">
<TextField
Expand All @@ -107,13 +114,25 @@ function Field({
// Use label from useWatch which is guaranteed to be the previous value
// since useWatch updates reactively (after re-render), not synchronously
const previousLabel = label || "";
hookForm.setValue(`${hookFieldNamespace}.label`, newLabel, { shouldDirty: true });
const currentIdentifier = hookForm.getValues(`${hookFieldNamespace}.identifier`);
hookForm.setValue(`${hookFieldNamespace}.label`, newLabel, {
shouldDirty: true,
});
const currentIdentifier = hookForm.getValues(
`${hookFieldNamespace}.identifier`
);
// Only auto-update identifier if it was auto-generated from the previous label
// This preserves manual identifier changes
const isIdentifierGeneratedFromPreviousLabel = currentIdentifier === getFieldIdentifier(previousLabel);
if (!currentIdentifier || isIdentifierGeneratedFromPreviousLabel) {
hookForm.setValue(`${hookFieldNamespace}.identifier`, getFieldIdentifier(newLabel), { shouldDirty: true });
const isIdentifierGeneratedFromPreviousLabel =
currentIdentifier === getFieldIdentifier(previousLabel);
if (
!currentIdentifier ||
isIdentifierGeneratedFromPreviousLabel
) {
hookForm.setValue(
`${hookFieldNamespace}.identifier`,
getFieldIdentifier(newLabel),
{ shouldDirty: true }
);
}
}}
/>
Expand All @@ -122,12 +141,23 @@ function Field({
<TextField
disabled={!!router}
label={t("identifier_url_parameter")}
hint={t("identifier_url_parameter_hint")}
name={`${hookFieldNamespace}.identifier`}
required
placeholder={t("identifies_name_field")}
value={identifier || routerField?.identifier || label || routerField?.label || ""}
value={
identifier ||
routerField?.identifier ||
label ||
routerField?.label ||
""
}
onChange={(e) => {
hookForm.setValue(`${hookFieldNamespace}.identifier`, e.target.value, { shouldDirty: true });
hookForm.setValue(
`${hookFieldNamespace}.identifier`,
e.target.value,
{ shouldDirty: true }
);
}}
/>
</div>
Expand All @@ -137,7 +167,9 @@ function Field({
control={hookForm.control}
defaultValue={routerField?.type}
render={({ field: { value, onChange } }) => {
const defaultValue = FieldTypes.find((fieldType) => fieldType.value === value);
const defaultValue = FieldTypes.find(
(fieldType) => fieldType.value === value
);
if (disableTypeChange) {
return (
<div className="data-testid-field-type">
Expand All @@ -150,9 +182,15 @@ function Field({
className={classNames(
"h-8 w-full justify-between text-left text-sm",
!!router && "bg-subtle cursor-not-allowed"
)}>
<span className="text-default">{defaultValue?.label || "Select field type"}</span>
<Icon name="chevron-down" className="text-default h-4 w-4" />
)}
>
<span className="text-default">
{defaultValue?.label || "Select field type"}
</span>
<Icon
name="chevron-down"
className="text-default h-4 w-4"
/>
</Button>
</Tooltip>
</div>
Expand Down Expand Up @@ -273,7 +311,9 @@ const FormEdit = ({
<div className="w-full py-4 lg:py-8">
<div ref={animationRef} className="flex w-full flex-col rounded-md">
{hookFormFields.map((field, key) => {
const existingField = Boolean((form.fields || []).find((f) => f.id === field.id));
const existingField = Boolean(
(form.fields || []).find((f) => f.id === field.id)
);
const hasFormResponses = (form._count?.responses ?? 0) > 0;
return (
<Field
Expand Down Expand Up @@ -310,7 +350,13 @@ const FormEdit = ({
</div>
{hookFormFields.length ? (
<div className={classNames("flex")}>
<Button data-testid="add-field" type="button" StartIcon="plus" color="secondary" onClick={addField}>
<Button
data-testid="add-field"
type="button"
StartIcon="plus"
color="secondary"
onClick={addField}
>
Add question
</Button>
</div>
Expand All @@ -319,7 +365,7 @@ const FormEdit = ({
) : (
<div className="w-full py-4 lg:py-8">
{/* TODO: remake empty screen for V3 */}
<div className="border-sublte bg-cal-muted flex flex-col items-center gap-6 rounded-xl border p-11">
<div className="border-subtle bg-cal-muted flex flex-col items-center gap-6 rounded-xl border p-11">
<div className="mb-3 grid">
{/* Icon card - Top */}
<div className="bg-default border-subtle z-30 col-start-1 col-end-1 row-start-1 row-end-1 h-10 w-10 transform rounded-md border shadow-sm">
Expand Down Expand Up @@ -350,7 +396,12 @@ const FormEdit = ({
Fields are the form fields that the booker would see.
</p>
</div>
<Button data-testid="add-field" onClick={addField} StartIcon="plus" className="mt-6">
<Button
data-testid="add-field"
onClick={addField}
StartIcon="plus"
className="mt-6"
>
Add question
</Button>
</div>
Expand All @@ -370,7 +421,9 @@ export default function FormEditPage({
{...props}
appUrl={appUrl}
permissions={permissions}
Page={({ hookForm, form }) => <FormEdit appUrl={appUrl} hookForm={hookForm} form={form} />}
Page={({ hookForm, form }) => (
<FormEdit appUrl={appUrl} hookForm={hookForm} form={form} />
)}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ vi.mock("@calcom/features/form/components/LocationSelect", () => {
};
});

const AttendeePhoneNumberLabel = "Attendee Phone Number";
const OrganizerPhoneLabel = "Organizer Phone Number";
const AttendeePhoneNumberLabel = "Attendee phone number";
const OrganizerPhoneLabel = "Organizer phone number";
const CampfireLabel = "Campfire";
const ZoomVideoLabel = "Zoom Video";
const OrganizerDefaultConferencingAppLabel = "Organizer's default app";
Expand Down
Loading
Loading