Skip to content

Commit 7c66f33

Browse files
pumfleetpedroccastrodevin-ai-integration[bot]
authored
chore: UX Fixes (#26643)
* Copy changes * Move search bar inline with new button * Get rid of no more results message * Change hidden badge to (hidden) * Remove Cal.ai badge from sidebar * Add dropdown to create button when there is multiple options * Fix delete dialog * Saved filters updates * More string fixes * Switch members table to use names * Fix member spacing * Fix routing form identifier field * Fix routing forms stuff * Only show SMS hint on SMS options * Make workflow delete button minimal * Fix padding on workflow steps * Remove min width on workflow title * Fix delete workflow PR * Fix org profile buttons * Fix org profile screen partially scrolled down * Improve logos & banner uploads * Personal profile fixes * Fix settings general view stuff * Sentence case consistency * Fix stuff I broke * Fix fab * Fix hidden translation string * Fix text fields * Make button small for solo users too * fix: update E2E tests to match sentence case labels in routing forms * fix: update tests to match sentence case label changes - insights.e2e.ts: chart titles (14 strings) - event-types.e2e.ts: Organizer phone number location - EditLocationDialog.test.tsx: phone number labels * fix: address Cubic AI review feedback (confidence 9+) - Replace hardcoded text-gray-500 with text-muted in TextField.tsx hint section - Replace text locator with data-testid in E2E test for location select Co-Authored-By: unknown <> * fix: update E2E tests for sentence case label changes - Use data-testid selectors for location options (more reliable than text) - Update field identifiers in routing-forms tests to match new labels - Fix Long text selector in manage-booking-questions test * fix: replace text locator with data-testid in manage-booking-questions E2E test Replace fragile text="Long text" locator with resilient page.getByTestId("select-option-textarea") selector per E2E best practices. Addresses Cubic AI review feedback (confidence 9/10). Co-Authored-By: unknown <> * fix: use .last() for multiple location select items --------- Co-authored-by: Pedro Castro <pedro@cal.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 32d97fb commit 7c66f33

File tree

31 files changed

+3349
-1886
lines changed

31 files changed

+3349
-1886
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"use client";
2+
3+
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
4+
import { useLocale } from "@calcom/lib/hooks/useLocale";
5+
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
6+
import type { ReactElement } from "react";
7+
import { useState } from "react";
8+
9+
import EventTypes, { EventTypesCTA, SearchContext } from "~/event-types/views/event-types-listing-view";
10+
11+
type GetUserEventGroupsResponse = Parameters<typeof EventTypesCTA>[0]["userEventGroupsData"];
12+
13+
const CTAWithContext = ({
14+
userEventGroupsData,
15+
}: {
16+
userEventGroupsData: GetUserEventGroupsResponse;
17+
}): ReactElement => {
18+
return <EventTypesCTA userEventGroupsData={userEventGroupsData} />;
19+
};
20+
21+
export function EventTypesWrapper({
22+
userEventGroupsData,
23+
user,
24+
}: {
25+
userEventGroupsData: GetUserEventGroupsResponse;
26+
user: {
27+
id: number;
28+
completedOnboarding?: boolean;
29+
} | null;
30+
}): ReactElement {
31+
const { t } = useLocale();
32+
const [searchTerm, setSearchTerm] = useState("");
33+
const debouncedSearchTerm = useDebounce(searchTerm, 500);
34+
35+
return (
36+
<SearchContext.Provider value={{ searchTerm, setSearchTerm, debouncedSearchTerm }}>
37+
<ShellMainAppDir
38+
heading={t("event_types_page_title")}
39+
subtitle={t("event_types_page_subtitle")}
40+
CTA={<CTAWithContext userEventGroupsData={userEventGroupsData} />}>
41+
<EventTypes userEventGroupsData={userEventGroupsData} user={user} />
42+
</ShellMainAppDir>
43+
</SearchContext.Provider>
44+
);
45+
}
Lines changed: 65 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,60 @@
1-
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
2-
import { createRouterCaller, getTRPCContext } from "app/_trpc/context";
3-
import type { PageProps, ReadonlyHeaders, ReadonlyRequestCookies } from "app/_types";
4-
import { _generateMetadata, getTranslate } from "app/_utils";
5-
import { unstable_cache } from "next/cache";
6-
import { cookies, headers } from "next/headers";
7-
import { redirect } from "next/navigation";
8-
9-
import { checkOnboardingRedirect } from "@calcom/features/auth/lib/onboardingUtils";
101
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
2+
import { checkOnboardingRedirect } from "@calcom/features/auth/lib/onboardingUtils";
113
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
4+
import type { RouterOutputs } from "@calcom/trpc/react";
125
import { eventTypesRouter } from "@calcom/trpc/server/routers/viewer/eventTypes/_router";
13-
146
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
7+
import { createRouterCaller, getTRPCContext } from "app/_trpc/context";
8+
import type {
9+
PageProps,
10+
ReadonlyHeaders,
11+
ReadonlyRequestCookies,
12+
} from "app/_types";
13+
import { _generateMetadata } from "app/_utils";
14+
import { unstable_cache } from "next/cache";
15+
import { cookies, headers } from "next/headers";
16+
import { redirect } from "next/navigation";
17+
import type { ReactElement } from "react";
1518

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

18-
export const generateMetadata = async () =>
19-
await _generateMetadata(
20-
(t) => t("event_types_page_title"),
21-
(t) => t("event_types_page_subtitle"),
22-
undefined,
23-
undefined,
24-
"/event-types"
21+
const getCachedEventGroups: (
22+
headers: ReadonlyHeaders,
23+
cookies: ReadonlyRequestCookies,
24+
filters?: {
25+
teamIds?: number[] | undefined;
26+
userIds?: number[] | undefined;
27+
upIds?: string[] | undefined;
28+
}
29+
) => Promise<RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]> =
30+
unstable_cache(
31+
async (
32+
headers: ReadonlyHeaders,
33+
cookies: ReadonlyRequestCookies,
34+
filters?: {
35+
teamIds?: number[] | undefined;
36+
userIds?: number[] | undefined;
37+
upIds?: string[] | undefined;
38+
}
39+
): Promise<RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]> => {
40+
const eventTypesCaller = await createRouterCaller(
41+
eventTypesRouter,
42+
await getTRPCContext(headers, cookies)
43+
);
44+
return await eventTypesCaller.getUserEventGroups({ filters });
45+
},
46+
["viewer.eventTypes.getUserEventGroups"],
47+
{ revalidate: 3600 } // seconds
2548
);
2649

27-
const getCachedEventGroups = unstable_cache(
28-
async (
29-
headers: ReadonlyHeaders,
30-
cookies: ReadonlyRequestCookies,
31-
filters?: {
32-
teamIds?: number[] | undefined;
33-
userIds?: number[] | undefined;
34-
upIds?: string[] | undefined;
35-
}
36-
) => {
37-
const eventTypesCaller = await createRouterCaller(
38-
eventTypesRouter,
39-
await getTRPCContext(headers, cookies)
40-
);
41-
return await eventTypesCaller.getUserEventGroups({ filters });
42-
},
43-
["viewer.eventTypes.getUserEventGroups"],
44-
{ revalidate: 3600 } // seconds
45-
);
46-
47-
const Page = async ({ searchParams }: PageProps) => {
50+
const Page = async ({ searchParams }: PageProps): Promise<ReactElement> => {
4851
const _searchParams = await searchParams;
4952
const _headers = await headers();
5053
const _cookies = await cookies();
5154

52-
const session = await getServerSession({ req: buildLegacyRequest(_headers, _cookies) });
55+
const session = await getServerSession({
56+
req: buildLegacyRequest(_headers, _cookies),
57+
});
5358
if (!session?.user?.id) {
5459
return redirect("/auth/login");
5560
}
@@ -65,18 +70,30 @@ const Page = async ({ searchParams }: PageProps) => {
6570
return redirect(onboardingPath);
6671
}
6772

68-
const t = await getTranslate();
6973
const filters = getTeamsFiltersFromQuery(_searchParams);
70-
const userEventGroupsData = await getCachedEventGroups(_headers, _cookies, filters);
74+
const userEventGroupsData = await getCachedEventGroups(
75+
_headers,
76+
_cookies,
77+
filters
78+
);
7179

7280
return (
73-
<ShellMainAppDir
74-
heading={t("event_types_page_title")}
75-
subtitle={t("event_types_page_subtitle")}
76-
CTA={<EventTypesCTA userEventGroupsData={userEventGroupsData} />}>
77-
<EventTypes userEventGroupsData={userEventGroupsData} user={session.user} />
78-
</ShellMainAppDir>
81+
<EventTypesWrapper
82+
userEventGroupsData={userEventGroupsData}
83+
user={session.user}
84+
/>
7985
);
8086
};
8187

88+
export const generateMetadata = async (): Promise<
89+
ReturnType<typeof _generateMetadata>
90+
> =>
91+
await _generateMetadata(
92+
(t) => t("event_types_page_title"),
93+
(t) => t("event_types_page_subtitle"),
94+
undefined,
95+
undefined,
96+
"/event-types"
97+
);
98+
8299
export default Page;

apps/web/app/(use-page-wrapper)/apps/routing-forms/[...pages]/FormEdit.tsx

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,16 @@ function Field({
8888
moveUp={moveUp}
8989
moveDown={moveDown}
9090
badge={
91-
router ? { text: router.name, variant: "gray", href: `${appUrl}/form-edit/${router.id}` } : null
91+
router
92+
? {
93+
text: router.name,
94+
variant: "gray",
95+
href: `${appUrl}/form-edit/${router.id}`,
96+
}
97+
: null
9298
}
93-
deleteField={router ? null : deleteField}>
99+
deleteField={router ? null : deleteField}
100+
>
94101
<FormCardBody>
95102
<div className="mb-3 w-full">
96103
<TextField
@@ -107,13 +114,25 @@ function Field({
107114
// Use label from useWatch which is guaranteed to be the previous value
108115
// since useWatch updates reactively (after re-render), not synchronously
109116
const previousLabel = label || "";
110-
hookForm.setValue(`${hookFieldNamespace}.label`, newLabel, { shouldDirty: true });
111-
const currentIdentifier = hookForm.getValues(`${hookFieldNamespace}.identifier`);
117+
hookForm.setValue(`${hookFieldNamespace}.label`, newLabel, {
118+
shouldDirty: true,
119+
});
120+
const currentIdentifier = hookForm.getValues(
121+
`${hookFieldNamespace}.identifier`
122+
);
112123
// Only auto-update identifier if it was auto-generated from the previous label
113124
// This preserves manual identifier changes
114-
const isIdentifierGeneratedFromPreviousLabel = currentIdentifier === getFieldIdentifier(previousLabel);
115-
if (!currentIdentifier || isIdentifierGeneratedFromPreviousLabel) {
116-
hookForm.setValue(`${hookFieldNamespace}.identifier`, getFieldIdentifier(newLabel), { shouldDirty: true });
125+
const isIdentifierGeneratedFromPreviousLabel =
126+
currentIdentifier === getFieldIdentifier(previousLabel);
127+
if (
128+
!currentIdentifier ||
129+
isIdentifierGeneratedFromPreviousLabel
130+
) {
131+
hookForm.setValue(
132+
`${hookFieldNamespace}.identifier`,
133+
getFieldIdentifier(newLabel),
134+
{ shouldDirty: true }
135+
);
117136
}
118137
}}
119138
/>
@@ -122,12 +141,23 @@ function Field({
122141
<TextField
123142
disabled={!!router}
124143
label={t("identifier_url_parameter")}
144+
hint={t("identifier_url_parameter_hint")}
125145
name={`${hookFieldNamespace}.identifier`}
126146
required
127147
placeholder={t("identifies_name_field")}
128-
value={identifier || routerField?.identifier || label || routerField?.label || ""}
148+
value={
149+
identifier ||
150+
routerField?.identifier ||
151+
label ||
152+
routerField?.label ||
153+
""
154+
}
129155
onChange={(e) => {
130-
hookForm.setValue(`${hookFieldNamespace}.identifier`, e.target.value, { shouldDirty: true });
156+
hookForm.setValue(
157+
`${hookFieldNamespace}.identifier`,
158+
e.target.value,
159+
{ shouldDirty: true }
160+
);
131161
}}
132162
/>
133163
</div>
@@ -137,7 +167,9 @@ function Field({
137167
control={hookForm.control}
138168
defaultValue={routerField?.type}
139169
render={({ field: { value, onChange } }) => {
140-
const defaultValue = FieldTypes.find((fieldType) => fieldType.value === value);
170+
const defaultValue = FieldTypes.find(
171+
(fieldType) => fieldType.value === value
172+
);
141173
if (disableTypeChange) {
142174
return (
143175
<div className="data-testid-field-type">
@@ -150,9 +182,15 @@ function Field({
150182
className={classNames(
151183
"h-8 w-full justify-between text-left text-sm",
152184
!!router && "bg-subtle cursor-not-allowed"
153-
)}>
154-
<span className="text-default">{defaultValue?.label || "Select field type"}</span>
155-
<Icon name="chevron-down" className="text-default h-4 w-4" />
185+
)}
186+
>
187+
<span className="text-default">
188+
{defaultValue?.label || "Select field type"}
189+
</span>
190+
<Icon
191+
name="chevron-down"
192+
className="text-default h-4 w-4"
193+
/>
156194
</Button>
157195
</Tooltip>
158196
</div>
@@ -273,7 +311,9 @@ const FormEdit = ({
273311
<div className="w-full py-4 lg:py-8">
274312
<div ref={animationRef} className="flex w-full flex-col rounded-md">
275313
{hookFormFields.map((field, key) => {
276-
const existingField = Boolean((form.fields || []).find((f) => f.id === field.id));
314+
const existingField = Boolean(
315+
(form.fields || []).find((f) => f.id === field.id)
316+
);
277317
const hasFormResponses = (form._count?.responses ?? 0) > 0;
278318
return (
279319
<Field
@@ -310,7 +350,13 @@ const FormEdit = ({
310350
</div>
311351
{hookFormFields.length ? (
312352
<div className={classNames("flex")}>
313-
<Button data-testid="add-field" type="button" StartIcon="plus" color="secondary" onClick={addField}>
353+
<Button
354+
data-testid="add-field"
355+
type="button"
356+
StartIcon="plus"
357+
color="secondary"
358+
onClick={addField}
359+
>
314360
Add question
315361
</Button>
316362
</div>
@@ -319,7 +365,7 @@ const FormEdit = ({
319365
) : (
320366
<div className="w-full py-4 lg:py-8">
321367
{/* TODO: remake empty screen for V3 */}
322-
<div className="border-sublte bg-cal-muted flex flex-col items-center gap-6 rounded-xl border p-11">
368+
<div className="border-subtle bg-cal-muted flex flex-col items-center gap-6 rounded-xl border p-11">
323369
<div className="mb-3 grid">
324370
{/* Icon card - Top */}
325371
<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">
@@ -350,7 +396,12 @@ const FormEdit = ({
350396
Fields are the form fields that the booker would see.
351397
</p>
352398
</div>
353-
<Button data-testid="add-field" onClick={addField} StartIcon="plus" className="mt-6">
399+
<Button
400+
data-testid="add-field"
401+
onClick={addField}
402+
StartIcon="plus"
403+
className="mt-6"
404+
>
354405
Add question
355406
</Button>
356407
</div>
@@ -370,7 +421,9 @@ export default function FormEditPage({
370421
{...props}
371422
appUrl={appUrl}
372423
permissions={permissions}
373-
Page={({ hookForm, form }) => <FormEdit appUrl={appUrl} hookForm={hookForm} form={form} />}
424+
Page={({ hookForm, form }) => (
425+
<FormEdit appUrl={appUrl} hookForm={hookForm} form={form} />
426+
)}
374427
/>
375428
</>
376429
);

apps/web/components/dialog/__tests__/EditLocationDialog.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ vi.mock("@calcom/features/form/components/LocationSelect", () => {
4646
};
4747
});
4848

49-
const AttendeePhoneNumberLabel = "Attendee Phone Number";
50-
const OrganizerPhoneLabel = "Organizer Phone Number";
49+
const AttendeePhoneNumberLabel = "Attendee phone number";
50+
const OrganizerPhoneLabel = "Organizer phone number";
5151
const CampfireLabel = "Campfire";
5252
const ZoomVideoLabel = "Zoom Video";
5353
const OrganizerDefaultConferencingAppLabel = "Organizer's default app";

0 commit comments

Comments
 (0)