Skip to content

Commit 80650df

Browse files
authored
Merge branch 'main' into devin/1763390447-add-org-trial-days
2 parents 92a7496 + 03ebabd commit 80650df

File tree

24 files changed

+245
-286
lines changed

24 files changed

+245
-286
lines changed

.github/workflows/integration-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ permissions:
55
contents: read
66
env:
77
NODE_OPTIONS: --max-old-space-size=4096
8+
INTEGRATION_TESTS: "true"
89
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
910
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
1011
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}

apps/api/v1/lib/utils/isAdmin.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ export const isAdminGuard = async (req: NextApiRequest) => {
1919
team: {
2020
isOrganization: true,
2121
organizationSettings: {
22-
is: {
23-
isAdminAPIEnabled: true,
24-
},
22+
isAdminAPIEnabled: true,
2523
},
2624
},
2725
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],

apps/api/v1/test/lib/bookings/[id]/_patch.integration-test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Request, Response } from "express";
22
import type { NextApiRequest, NextApiResponse } from "next";
33
import { createMocks } from "node-mocks-http";
4-
import { describe, it, expect } from "vitest";
4+
import { describe, it, expect, beforeAll } from "vitest";
55

66
import prisma from "@calcom/prisma";
77

@@ -11,6 +11,30 @@ type CustomNextApiRequest = NextApiRequest & Request;
1111
type CustomNextApiResponse = NextApiResponse & Response;
1212

1313
describe("PATCH /api/bookings", () => {
14+
beforeAll(async () => {
15+
const acmeOrg = await prisma.team.findFirst({
16+
where: {
17+
slug: "acme",
18+
isOrganization: true,
19+
},
20+
});
21+
22+
if (acmeOrg) {
23+
await prisma.organizationSettings.upsert({
24+
where: {
25+
organizationId: acmeOrg.id,
26+
},
27+
update: {
28+
isAdminAPIEnabled: true,
29+
},
30+
create: {
31+
organizationId: acmeOrg.id,
32+
orgAutoAcceptEmail: "acme.com",
33+
isAdminAPIEnabled: true,
34+
},
35+
});
36+
}
37+
});
1438
it("Returns 403 when user has no permission to the booking", async () => {
1539
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "[email protected]" } });
1640
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "[email protected]" } });

apps/api/v1/test/lib/bookings/_get.integration-test.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,33 @@ const DefaultPagination = {
1616
skip: 0,
1717
};
1818

19-
describe("GET /api/bookings", () => {
20-
let proUser: Awaited<ReturnType<typeof prisma.user.findFirstOrThrow>>;
21-
let proUserBooking: Awaited<ReturnType<typeof prisma.booking.findFirstOrThrow>>;
22-
19+
describe("GET /api/bookings", async () => {
2320
beforeAll(async () => {
24-
proUser = await prisma.user.findFirstOrThrow({ where: { email: "[email protected]" } });
25-
proUserBooking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
21+
const acmeOrg = await prisma.team.findFirst({
22+
where: {
23+
slug: "acme",
24+
isOrganization: true,
25+
},
26+
});
27+
28+
if (acmeOrg) {
29+
await prisma.organizationSettings.upsert({
30+
where: {
31+
organizationId: acmeOrg.id,
32+
},
33+
update: {
34+
isAdminAPIEnabled: true,
35+
},
36+
create: {
37+
organizationId: acmeOrg.id,
38+
orgAutoAcceptEmail: "acme.com",
39+
isAdminAPIEnabled: true,
40+
},
41+
});
42+
}
2643
});
44+
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "[email protected]" } });
45+
const proUserBooking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
2746

2847
it("Does not return bookings of other users when user has no permission", async () => {
2948
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "[email protected]" } });

apps/api/v1/test/lib/utils/isAdmin.integration-test.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Request, Response } from "express";
22
import type { NextApiRequest, NextApiResponse } from "next";
33
import { createMocks } from "node-mocks-http";
4-
import { describe, it, expect } from "vitest";
4+
import { describe, it, expect, beforeAll } from "vitest";
55

66
import prisma from "@calcom/prisma";
77

@@ -12,6 +12,53 @@ type CustomNextApiRequest = NextApiRequest & Request;
1212
type CustomNextApiResponse = NextApiResponse & Response;
1313

1414
describe("isAdmin guard", () => {
15+
beforeAll(async () => {
16+
const acmeOrg = await prisma.team.findFirst({
17+
where: {
18+
slug: "acme",
19+
isOrganization: true,
20+
},
21+
});
22+
23+
if (acmeOrg) {
24+
await prisma.organizationSettings.upsert({
25+
where: {
26+
organizationId: acmeOrg.id,
27+
},
28+
update: {
29+
isAdminAPIEnabled: true,
30+
},
31+
create: {
32+
organizationId: acmeOrg.id,
33+
orgAutoAcceptEmail: "acme.com",
34+
isAdminAPIEnabled: true,
35+
},
36+
});
37+
}
38+
39+
const dunderOrg = await prisma.team.findFirst({
40+
where: {
41+
slug: "dunder-mifflin",
42+
isOrganization: true,
43+
},
44+
});
45+
46+
if (dunderOrg) {
47+
await prisma.organizationSettings.upsert({
48+
where: {
49+
organizationId: dunderOrg.id,
50+
},
51+
update: {
52+
isAdminAPIEnabled: false,
53+
},
54+
create: {
55+
organizationId: dunderOrg.id,
56+
orgAutoAcceptEmail: "dunder-mifflin.com",
57+
isAdminAPIEnabled: false,
58+
},
59+
});
60+
}
61+
});
1562
it("Returns false when user does not exist in the system", async () => {
1663
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
1764
method: "POST",

apps/api/v1/test/lib/utils/retrieveScopedAccessibleUsers.integration-test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect } from "vitest";
1+
import { describe, it, expect, beforeAll } from "vitest";
22

33
import prisma from "@calcom/prisma";
44

@@ -8,6 +8,30 @@ import {
88
} from "../../../lib/utils/retrieveScopedAccessibleUsers";
99

1010
describe("retrieveScopedAccessibleUsers tests", () => {
11+
beforeAll(async () => {
12+
const acmeOrg = await prisma.team.findFirst({
13+
where: {
14+
slug: "acme",
15+
isOrganization: true,
16+
},
17+
});
18+
19+
if (acmeOrg) {
20+
await prisma.organizationSettings.upsert({
21+
where: {
22+
organizationId: acmeOrg.id,
23+
},
24+
update: {
25+
isAdminAPIEnabled: true,
26+
},
27+
create: {
28+
organizationId: acmeOrg.id,
29+
orgAutoAcceptEmail: "acme.com",
30+
isAdminAPIEnabled: true,
31+
},
32+
});
33+
}
34+
});
1135
describe("getAccessibleUsers", () => {
1236
it("Does not return members when only admin user ID is supplied", async () => {
1337
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "[email protected]" } });

apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export class BookingsController_2024_08_13 {
189189
2. uid of one of the recurring booking recurrences
190190
191191
3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).
192-
192+
193193
If you are fetching a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
194194
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
195195
@@ -213,7 +213,7 @@ export class BookingsController_2024_08_13 {
213213
@ApiOperation({
214214
summary: "Get all the recordings for the booking",
215215
description: `Fetches all the recordings for the booking \`:bookingUid\`
216-
216+
217217
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
218218
`,
219219
})
@@ -229,9 +229,9 @@ export class BookingsController_2024_08_13 {
229229
@Get("/:bookingUid/transcripts")
230230
@UseGuards(BookingUidGuard)
231231
@ApiOperation({
232-
summary: "Get all the transcripts download links for the booking",
232+
summary: "Get Cal Video real time transcript download links for the booking",
233233
description: `Fetches all the transcripts download links for the booking \`:bookingUid\`
234-
234+
235235
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
236236
`,
237237
})
@@ -279,7 +279,7 @@ export class BookingsController_2024_08_13 {
279279
@ApiOperation({
280280
summary: "Reschedule a booking",
281281
description: `Reschedule a booking or seated booking
282-
282+
283283
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
284284
`,
285285
})
@@ -321,19 +321,19 @@ export class BookingsController_2024_08_13 {
321321
@ApiOperation({
322322
summary: "Cancel a booking",
323323
description: `:bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.
324-
324+
325325
\nCancelling normal bookings:
326326
If the booking is not seated and not recurring, simply pass :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and optionally cancellationReason in the request body \`{"cancellationReason": "Will travel"}\`.
327327
328328
\nCancelling seated bookings:
329329
It is possible to cancel specific seat within a booking as an attendee or all of the seats as the host.
330330
\n1. As an attendee - provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and seatUid in the request body \`{"seatUid": "123-123-123"}\` . This will remove this particular attendance from the booking.
331331
\n2. As the host or org admin of host - host can cancel booking for all attendees aka for every seat, this also applies to org admins. Provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and cancellationReason in the request body \`{"cancellationReason": "Will travel"}\` and \`Authorization: Bearer token\` request header where token is event type owner (host) credential. This will cancel the booking for all attendees.
332-
332+
333333
\nCancelling recurring seated bookings:
334334
For recurring seated bookings it is not possible to cancel all of them with 1 call
335335
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.
336-
336+
337337
If you are cancelling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
338338
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
339339
@@ -374,7 +374,7 @@ export class BookingsController_2024_08_13 {
374374
@ApiOperation({
375375
summary: "Mark a booking absence",
376376
description: `The provided authorization header refers to the owner of the booking.
377-
377+
378378
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
379379
`,
380380
})
@@ -399,7 +399,7 @@ export class BookingsController_2024_08_13 {
399399
@ApiOperation({
400400
summary: "Reassign a booking to auto-selected host",
401401
description: `Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.
402-
402+
403403
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
404404
`,
405405
})
@@ -423,7 +423,7 @@ export class BookingsController_2024_08_13 {
423423
@ApiOperation({
424424
summary: "Reassign a booking to a specific host",
425425
description: `Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.
426-
426+
427427
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
428428
`,
429429
})
@@ -454,7 +454,7 @@ export class BookingsController_2024_08_13 {
454454
@ApiOperation({
455455
summary: "Confirm a booking",
456456
description: `The provided authorization header refers to the owner of the booking.
457-
457+
458458
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
459459
`,
460460
})
@@ -478,8 +478,8 @@ export class BookingsController_2024_08_13 {
478478
@ApiOperation({
479479
summary: "Decline a booking",
480480
description: `The provided authorization header refers to the owner of the booking.
481-
482-
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
481+
482+
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
483483
`,
484484
})
485485
async declineBooking(
@@ -502,8 +502,8 @@ export class BookingsController_2024_08_13 {
502502
@ApiOperation({
503503
summary: "Get 'Add to Calendar' links for a booking",
504504
description: `Retrieve calendar links for a booking that can be used to add the event to various calendar services. Returns links for Google Calendar, Microsoft Office, Microsoft Outlook, and a downloadable ICS file.
505-
506-
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
505+
506+
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
507507
`,
508508
})
509509
@HttpCode(HttpStatus.OK)

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { v4 as uuidv4 } from "uuid";
88

99
import { FieldTypes } from "@calcom/app-store/routing-forms/lib/FieldTypes";
1010
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
11+
import { getFieldIdentifier } from "@calcom/features/form-builder/utils/getFieldIdentifier";
1112
import { useLocale } from "@calcom/lib/hooks/useLocale";
1213
import classNames from "@calcom/ui/classNames";
1314
import { Button } from "@calcom/ui/components/button";
@@ -102,14 +103,25 @@ function Field({
102103
required
103104
{...hookForm.register(`${hookFieldNamespace}.label`)}
104105
onChange={(e) => {
105-
hookForm.setValue(`${hookFieldNamespace}.label`, e.target.value, { shouldDirty: true });
106+
const newLabel = e.target.value;
107+
// Use label from useWatch which is guaranteed to be the previous value
108+
// since useWatch updates reactively (after re-render), not synchronously
109+
const previousLabel = label || "";
110+
hookForm.setValue(`${hookFieldNamespace}.label`, newLabel, { shouldDirty: true });
111+
const currentIdentifier = hookForm.getValues(`${hookFieldNamespace}.identifier`);
112+
// Only auto-update identifier if it was auto-generated from the previous label
113+
// This preserves manual identifier changes
114+
const isIdentifierGeneratedFromPreviousLabel = currentIdentifier === getFieldIdentifier(previousLabel);
115+
if (!currentIdentifier || isIdentifierGeneratedFromPreviousLabel) {
116+
hookForm.setValue(`${hookFieldNamespace}.identifier`, getFieldIdentifier(newLabel), { shouldDirty: true });
117+
}
106118
}}
107119
/>
108120
</div>
109121
<div className="mb-3 w-full">
110122
<TextField
111123
disabled={!!router}
112-
label="Identifier"
124+
label={t("identifier_url_parameter")}
113125
name={`${hookFieldNamespace}.identifier`}
114126
required
115127
placeholder={t("identifies_name_field")}

apps/web/components/apps/routing-forms/Header.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const useRoutingFormNavigation = (
2323
) => {
2424
const pathname = usePathname();
2525
const router = useRouter();
26-
const formContext = useFormContext<RoutingFormWithResponseCount>();
26+
const { isDirty } = useFormContext<RoutingFormWithResponseCount>().formState;
2727

2828
// Get the current page based on the pathname since we use a custom routing system
2929
const getCurrentPage = () => {
@@ -39,7 +39,7 @@ const useRoutingFormNavigation = (
3939

4040
const baseUrl = `${appUrl}/${value}/${form.id}`;
4141

42-
if (value === "route-builder" && formContext.formState.isDirty) {
42+
if (value === "route-builder" && isDirty) {
4343
setShowInfoLostDialog(true);
4444
} else {
4545
router.push(baseUrl);

0 commit comments

Comments
 (0)