Skip to content

Commit a1563f3

Browse files
committed
Merge branch 'main' of https://github.com/calcom/cal.com into chore/improve-workflows-ui
2 parents 9e7627e + 2e96600 commit a1563f3

71 files changed

Lines changed: 812 additions & 371 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/knowledge-base.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,3 +318,12 @@ dates.map((date) => dayjs.utc(date).add(1, "day").format());
318318
// ✅ Best - Use native Date when possible
319319
dates.map((date) => new Date(date.valueOf() + 24 * 60 * 60 * 1000));
320320
```
321+
322+
## Avoid using Dayjs if you don’t need to be strictly tz aware.
323+
324+
When doing logic like Dayjs.startOf(".."), you can instead use date-fns' `startOfMonth(dateObj)` / `endOfDay(dateObj)`;
325+
When doing logic that depends on Browser locale, use i18n.language (prefer to deconstruct) like: `const { i18n: { language } } = useLocale();`, in combination with built-in Intl.
326+
327+
Note that with Date, you’re dealing with System time, so it’s not suited to everywhere (such as in the Booker, where instead we’ll likely migrate to Temporal) - but in most cases the above are suitable.
328+
329+
The main reason for doing so is that Dayjs uses a useful, but highly risky plugin system, which has led us to create `@calcom/dayjs` - this is heavy however, because it pre-loads ALL plugins, including locale handling. It’s a non-ideal solution to a problem that unfortunately exists due to Dayjs.

apps/api/v2/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"@axiomhq/winston": "^1.2.0",
3939
"@calcom/platform-constants": "*",
4040
"@calcom/platform-enums": "*",
41-
"@calcom/platform-libraries": "npm:@calcom/[email protected].349",
41+
"@calcom/platform-libraries": "npm:@calcom/[email protected].350",
4242
"@calcom/platform-types": "*",
4343
"@calcom/platform-utils": "*",
4444
"@calcom/prisma": "*",

apps/web/app/(use-page-wrapper)/settings/(admin-layout)/AdminLayoutAppDirClient.tsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,19 @@
11
"use client";
22

3-
import { usePathname, useRouter } from "next/navigation";
3+
import { usePathname } from "next/navigation";
44
import type { ComponentProps } from "react";
5-
import React, { useEffect } from "react";
5+
import React from "react";
66

77
import type Shell from "@calcom/features/shell/Shell";
8-
import { UserPermissionRole } from "@calcom/prisma/enums";
8+
import type { UserPermissionRole } from "@calcom/prisma/enums";
99
import { ErrorBoundary } from "@calcom/ui/components/errorBoundary";
1010

1111
export type AdminLayoutProps = {
1212
children: React.ReactNode;
1313
userRole: UserPermissionRole | "INACTIVE_ADMIN" | undefined;
1414
} & ComponentProps<typeof Shell>;
15-
export default function AdminLayoutAppDirClient({ userRole, children }: AdminLayoutProps) {
15+
export default function AdminLayoutAppDirClient({ children }: AdminLayoutProps) {
1616
const pathname = usePathname();
17-
const router = useRouter();
18-
19-
// Force redirect on component level
20-
useEffect(() => {
21-
if (userRole !== UserPermissionRole.ADMIN) {
22-
router.replace("/settings/my-account/profile");
23-
}
24-
}, [userRole, router]);
2517

2618
const isAppsPage = pathname?.startsWith("/settings/admin/apps");
2719
return (

apps/web/app/(use-page-wrapper)/settings/(admin-layout)/layout.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { cookies, headers } from "next/headers";
2+
import { redirect } from "next/navigation";
23
import React from "react";
34

45
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
6+
import { UserPermissionRole } from "@calcom/prisma/enums";
57

68
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
79

@@ -15,5 +17,9 @@ export default async function AdminLayoutAppDir(props: AdminLayoutAppDirProps) {
1517
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
1618
const userRole = session?.user?.role;
1719

20+
if (userRole !== UserPermissionRole.ADMIN) {
21+
return redirect("/settings/my-account/profile");
22+
}
23+
1824
return await SettingsLayoutAppDir({ children: <AdminLayoutAppDirClient {...props} userRole={userRole} /> });
1925
}

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"handlebars": "^4.7.7",
9494
"ical.js": "^1.4.0",
9595
"ics": "^2.37.0",
96+
"isbot": "^5.1.30",
9697
"jose": "^4.13.1",
9798
"jotai": "^2.12.2",
9899
"jsdom": "^22.0.0",

apps/web/server/lib/router/getServerSideProps.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,35 @@
11
import { wrapGetServerSidePropsWithSentry } from "@sentry/nextjs";
2+
import { isbot } from "isbot";
23
import type { GetServerSidePropsContext } from "next";
34

45
import { getRoutedUrl, hasEmbedPath } from "@calcom/features/routing-forms/lib/getRoutedUrl";
6+
import logger from "@calcom/lib/logger";
57

68
import { TRPCError } from "@trpc/server";
79

10+
const log = logger.getSubLogger({ prefix: ["router/getServerSideProps"] });
11+
12+
// Show a positive message to the bots so that they don't think that the form is broken.
13+
const BOT_MESSAGE = "Thank you for your interest! We will be in touch soon.";
14+
815
export const getServerSideProps = wrapGetServerSidePropsWithSentry(async function getServerSideProps(
916
context: GetServerSidePropsContext
1017
) {
1118
try {
19+
// Check if the request is from a bot
20+
const userAgent = context.req.headers["user-agent"];
21+
if (userAgent && isbot(userAgent)) {
22+
log.info(`[BOT] Ignoring request from user agent: ${userAgent}`);
23+
return {
24+
props: {
25+
isEmbed: hasEmbedPath(context.req.url || ""),
26+
form: null,
27+
message: BOT_MESSAGE,
28+
errorMessage: null,
29+
},
30+
};
31+
}
32+
1233
return await getRoutedUrl(context);
1334
} catch (error) {
1435
if (error instanceof TRPCError && error.code === "TOO_MANY_REQUESTS") {
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import {
2+
createBookingScenario,
3+
getOrganizer,
4+
getScenarioData,
5+
TestData,
6+
mockSuccessfulVideoMeetingCreation,
7+
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
8+
9+
import { describe, it, beforeEach, vi, expect } from "vitest";
10+
11+
import * as handleConfirmationModule from "@calcom/features/bookings/lib/handleConfirmation";
12+
import { BookingStatus } from "@calcom/prisma/enums";
13+
import { confirmHandler } from "@calcom/trpc/server/routers/viewer/bookings/confirm.handler";
14+
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
15+
16+
describe("confirmHandler", () => {
17+
beforeEach(() => {
18+
vi.clearAllMocks();
19+
});
20+
21+
it("should pass hideCalendarNotes property to CalendarEvent when enabled", async () => {
22+
vi.setSystemTime("2050-01-07T00:00:00Z");
23+
24+
const handleConfirmationSpy = vi.spyOn(handleConfirmationModule, "handleConfirmation");
25+
26+
const attendeeUser = getOrganizer({
27+
28+
name: "test name",
29+
id: 102,
30+
schedules: [TestData.schedules.IstWorkHours],
31+
});
32+
33+
const organizer = getOrganizer({
34+
name: "Organizer",
35+
36+
id: 101,
37+
schedules: [TestData.schedules.IstWorkHours],
38+
});
39+
40+
const uidOfBooking = "hideNotes123";
41+
const iCalUID = `${uidOfBooking}@Cal.com`;
42+
43+
const plus1DateString = "2050-01-08";
44+
45+
await createBookingScenario(
46+
getScenarioData({
47+
eventTypes: [
48+
{
49+
id: 1,
50+
slotInterval: 15,
51+
length: 15,
52+
locations: [],
53+
hideCalendarNotes: true,
54+
hideCalendarEventDetails: true,
55+
requiresConfirmation: true,
56+
users: [
57+
{
58+
id: 101,
59+
},
60+
],
61+
},
62+
],
63+
bookings: [
64+
{
65+
id: 101,
66+
uid: uidOfBooking,
67+
eventTypeId: 1,
68+
status: BookingStatus.PENDING,
69+
startTime: `${plus1DateString}T05:00:00.000Z`,
70+
endTime: `${plus1DateString}T05:15:00.000Z`,
71+
references: [],
72+
iCalUID,
73+
location: "integrations:daily",
74+
attendees: [attendeeUser],
75+
responses: { name: attendeeUser.name, email: attendeeUser.email, notes: "Sensitive information" },
76+
},
77+
],
78+
organizer,
79+
apps: [TestData.apps["daily-video"]],
80+
})
81+
);
82+
83+
mockSuccessfulVideoMeetingCreation({
84+
metadataLookupKey: "dailyvideo",
85+
});
86+
87+
const ctx = {
88+
user: {
89+
id: organizer.id,
90+
name: organizer.name,
91+
timeZone: organizer.timeZone,
92+
username: organizer.username,
93+
} as NonNullable<TrpcSessionUser>,
94+
};
95+
96+
const res = await confirmHandler({
97+
ctx,
98+
input: { bookingId: 101, confirmed: true, reason: "", emailsEnabled: true },
99+
});
100+
101+
expect(res?.status).toBe(BookingStatus.ACCEPTED);
102+
expect(handleConfirmationSpy).toHaveBeenCalledTimes(1);
103+
104+
const handleConfirmationCall = handleConfirmationSpy.mock.calls[0][0];
105+
const calendarEvent = handleConfirmationCall.evt;
106+
107+
expect(calendarEvent.hideCalendarNotes).toBe(true);
108+
expect(calendarEvent.hideCalendarEventDetails).toBe(true);
109+
});
110+
});

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
"@snaplet/copycat": "^4.1.0",
9292
"@testing-library/jest-dom": "^5.16.5",
9393
"@testing-library/react": "^16.0.1",
94-
"@vitest/ui": "^2.1.1",
94+
"@vitest/ui": "^2.1.9",
9595
"c8": "^7.13.0",
9696
"checkly": "latest",
9797
"dotenv-checker": "^1.1.5",
@@ -110,7 +110,7 @@
110110
"resize-observer-polyfill": "^1.5.1",
111111
"tsc-absolute": "^1.0.0",
112112
"typescript": "5.9.0-beta",
113-
"vitest": "^2.1.1",
113+
"vitest": "^2.1.9",
114114
"vitest-fetch-mock": "^0.3.0",
115115
"vitest-mock-extended": "^2.0.2"
116116
},

packages/features/Segment.test.tsx

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { render, screen, waitFor } from "@testing-library/react";
2+
import React from "react";
23
import { describe, expect, it, vi, beforeEach } from "vitest";
34
import type { Mock } from "vitest";
45

@@ -49,9 +50,10 @@ const mockAttributesWithSingleSelect = () => {
4950
slug: "department",
5051
name: "Department",
5152
type: AttributeType.SINGLE_SELECT,
53+
isWeightsEnabled: false,
5254
options: [
53-
{ id: "1", value: "Sales", slug: "sales" },
54-
{ id: "2", value: "Engineering", slug: "engineering" },
55+
{ id: "1", value: "Sales", slug: "sales", contains: [], isGroup: false },
56+
{ id: "2", value: "Engineering", slug: "engineering", contains: [], isGroup: false },
5557
],
5658
},
5759
],
@@ -85,9 +87,24 @@ vi.mock("@calcom/lib/hooks/useLocale", () => ({
8587
}));
8688

8789
describe("Segment", () => {
90+
const defaultQueryValue = {
91+
id: "root",
92+
type: "group",
93+
children1: {
94+
"rule-1": {
95+
type: "rule",
96+
properties: {
97+
field: "department",
98+
operator: "select_equals",
99+
value: ["Sales"],
100+
},
101+
},
102+
},
103+
} as AttributesQueryValue;
104+
88105
const defaultProps = {
89106
teamId: 1,
90-
queryValue: null as AttributesQueryValue | null,
107+
queryValue: defaultQueryValue,
91108
onQueryValueChange: vi.fn(),
92109
className: "test-class",
93110
};
@@ -132,7 +149,25 @@ describe("Segment", () => {
132149
});
133150
});
134151

135-
it("shows matching team members when query value is provided", async () => {
152+
it("shows no filter selected message when empty query value is provided", async () => {
153+
mockGetMatchingTeamMembers({
154+
isPending: true,
155+
data: undefined,
156+
});
157+
158+
const emptyQueryValue = {
159+
id: "root",
160+
type: "group",
161+
children1: {},
162+
} as AttributesQueryValue;
163+
164+
render(<Segment {...defaultProps} queryValue={emptyQueryValue} />);
165+
await waitFor(() => {
166+
expect(screen.getByText("no_filter_set")).toBeInTheDocument();
167+
});
168+
});
169+
170+
it("shows matching team members when valid query value is provided", async () => {
136171
mockGetMatchingTeamMembers({
137172
isPending: false,
138173
data: {
@@ -149,12 +184,7 @@ describe("Segment", () => {
149184
},
150185
});
151186

152-
const queryValue = {
153-
id: "root",
154-
type: "group",
155-
} as AttributesQueryValue;
156-
157-
render(<Segment {...defaultProps} queryValue={queryValue} />);
187+
render(<Segment {...defaultProps} queryValue={defaultQueryValue} />);
158188
await waitFor(() => {
159189
expect(screen.getByText("John Doe")).toBeInTheDocument();
160190
expect(screen.getByText("([email protected])")).toBeInTheDocument();

packages/features/Segment.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ function MatchingTeamMembers({
125125
}
126126
);
127127

128+
if (!hasValidValue) {
129+
return (
130+
<div className="border-subtle bg-muted mt-4 space-y-3 rounded-md border p-4">
131+
<div className="text-subtle flex items-center text-sm font-medium">
132+
<span>{t("no_filter_set")}</span>
133+
</div>
134+
</div>
135+
);
136+
}
137+
128138
if (isPending) {
129139
return (
130140
<div
@@ -149,23 +159,14 @@ function MatchingTeamMembers({
149159

150160
if (!matchingTeamMembersWithResult) return <span>{t("something_went_wrong")}</span>;
151161
const { result: matchingTeamMembers } = matchingTeamMembersWithResult;
152-
if (!matchingTeamMembers || !queryValue) {
153-
return (
154-
<div className="border-subtle bg-muted mt-4 space-y-3 rounded-md border p-4">
155-
<div className="text-subtle flex items-center text-sm font-medium">
156-
<span>{t("no_filter_set")}</span>
157-
</div>
158-
</div>
159-
);
160-
}
161162

162163
return (
163164
<div className="border-subtle bg-muted mt-4 space-y-3 rounded-md border p-4">
164165
<div className="text-emphasis flex items-center text-sm font-medium">
165-
<span>{t("x_matching_members", { x: matchingTeamMembers.length })}</span>
166+
<span>{t("x_matching_members", { x: matchingTeamMembers?.length ?? 0 })}</span>
166167
</div>
167168
<ul className="divide-subtle divide-y">
168-
{matchingTeamMembers.map((member) => (
169+
{matchingTeamMembers?.map((member) => (
169170
<li key={member.id} className="flex items-center py-2">
170171
<div className="flex flex-1 items-center space-x-2 text-sm">
171172
<span className="font-medium">{member.name}</span>

0 commit comments

Comments
 (0)