From c52eff65a21be3ba234786121557340fe5d732e6 Mon Sep 17 00:00:00 2001 From: Volnei Munhoz Date: Tue, 30 Dec 2025 08:46:26 -0300 Subject: [PATCH 01/11] chore: fix biome warnings/errors (#26295) * Fix icon-names * make all errors into warns * Minimal fix to mock --- .../app/api/defaultResponderForAppDir.test.ts | 29 +- apps/web/package.json | 1 + biome.json | 72 ++-- companion/biome.json | 1 + example-apps/credential-sync/package.json | 3 +- package.json | 2 +- packages/coss-ui/package.json | 5 +- packages/embeds/embed-core/package.json | 1 + packages/lib/package.json | 1 + packages/platform/examples/base/package.json | 1 - packages/trpc/package.json | 1 + packages/ui/components/icon/icon-names.ts | 310 +++++++++--------- packages/ui/scripts/build-icons.mjs | 27 +- yarn.lock | 9 +- 14 files changed, 246 insertions(+), 217 deletions(-) diff --git a/apps/web/app/api/defaultResponderForAppDir.test.ts b/apps/web/app/api/defaultResponderForAppDir.test.ts index 1173fe7ccd7ae2..dc8e7c48c873c7 100644 --- a/apps/web/app/api/defaultResponderForAppDir.test.ts +++ b/apps/web/app/api/defaultResponderForAppDir.test.ts @@ -8,15 +8,18 @@ import { TRPCError } from "@trpc/server"; import { defaultResponderForAppDir } from "./defaultResponderForAppDir"; -vi.mock("next/server", () => ({ - NextRequest: class MockNextRequest {}, - NextResponse: { - json: vi.fn((body, init) => ({ - json: vi.fn().mockResolvedValue(body), - status: init?.status || 200, - })), - }, -})); +vi.mock("next/server", () => { + class MockNextRequest extends Request {} + return { + NextRequest: MockNextRequest, + NextResponse: { + json: vi.fn((body, init) => ({ + json: vi.fn().mockResolvedValue(body), + status: init?.status || 200, + })), + }, + }; +}); describe("defaultResponderForAppDir", () => { it("should return a JSON response when handler resolves with a result", async () => { @@ -44,7 +47,9 @@ describe("defaultResponderForAppDir", () => { }); it("should respond with status code 409 for NoAvailableUsersFound", async () => { - const f = vi.fn().mockRejectedValue(new Error(ErrorCode.NoAvailableUsersFound)); + const f = vi + .fn() + .mockRejectedValue(new Error(ErrorCode.NoAvailableUsersFound)); const req = { method: "GET", url: "/api/test" } as unknown as NextRequest; const params = Promise.resolve({}); @@ -60,7 +65,9 @@ describe("defaultResponderForAppDir", () => { }); it("should respond with a 429 status code for rate limit errors", async () => { - const f = vi.fn().mockRejectedValue(new TRPCError({ code: "TOO_MANY_REQUESTS" })); + const f = vi + .fn() + .mockRejectedValue(new TRPCError({ code: "TOO_MANY_REQUESTS" })); const req = { method: "POST", url: "/api/test" } as unknown as NextRequest; const params = Promise.resolve({}); diff --git a/apps/web/package.json b/apps/web/package.json index f35468f7bf9b91..5ff666a3317e9d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -151,6 +151,7 @@ }, "devDependencies": { "@babel/core": "7.26.10", + "@biomejs/biome": "2.3.10", "@calcom/config": "workspace:*", "@calcom/types": "workspace:*", "@microsoft/microsoft-graph-types-beta": "0.15.0-preview", diff --git a/biome.json b/biome.json index 0eb530165696f8..055095ed7786f7 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", "root": true, "css": { "parser": { @@ -21,26 +21,29 @@ "files": { "includes": [ "**/*", - "!**/node_modules", - "!**/.next", - "!**/.turbo", - "!**/dist", - "!**/build", - "!**/public", - "!public", - "!apps/web/public/embed", - "!packages/prisma/zod", - "!packages/prisma/enums", - "!*.lock", - "!*.log", - "!.gitignore", - "!.npmignore", - "!.prettierignore", - "!.eslintignore", - "!.DS_Store", - "!coverage", - "!lint-results", - "!test-results" + "!!**/node_modules", + "!!**/.next", + "!!**/.turbo", + "!!**/dist", + "!!**/build", + "!!**/public", + "!!public", + "!!apps/web/public/embed", + "!!packages/prisma/zod", + "!!packages/prisma/enums", + "!!*.lock", + "!!*.log", + "!!.gitignore", + "!!.npmignore", + "!!.prettierignore", + "!!.eslintignore", + "!!.DS_Store", + "!!coverage", + "!!lint-results", + "!!test-results", + "!!packages/lib/raqb/resolveQueryValue.test.ts", + "!!packages/ui/components/editor/plugins/ToolbarPlugin.tsx", + "!!packages/embeds/embed-core/src/lib/domUtils.ts" ] }, "javascript": { @@ -58,17 +61,14 @@ "enabled": true, "rules": { "recommended": true, - "style": { - "useNodejsImportProtocol": "warn" - }, - "correctness": { - "noUnusedVariables": { - "level": "warn", - "options": { - "ignoreRestSiblings": true - } - } - } + "correctness": "warn", + "suspicious": "warn", + "complexity": "warn", + "performance": "warn", + "nursery": "warn", + "a11y": "warn", + "style": "warn", + "security": "warn" } }, "overrides": [ @@ -78,7 +78,7 @@ "rules": { "style": { "noRestrictedImports": { - "level": "error", + "level": "warn", "options": { "patterns": [ { @@ -110,7 +110,7 @@ "rules": { "style": { "noRestrictedImports": { - "level": "error", + "level": "warn", "options": { "patterns": [ { @@ -134,7 +134,7 @@ "rules": { "style": { "noRestrictedImports": { - "level": "error", + "level": "warn", "options": { "patterns": [ { @@ -154,7 +154,7 @@ "rules": { "style": { "noRestrictedImports": { - "level": "error", + "level": "warn", "options": { "patterns": [ { diff --git a/companion/biome.json b/companion/biome.json index 98cc685dec8b6a..75163f8b39115f 100644 --- a/companion/biome.json +++ b/companion/biome.json @@ -1,5 +1,6 @@ { "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", + "root": false, "vcs": { "enabled": true, "clientKind": "git", diff --git a/example-apps/credential-sync/package.json b/example-apps/credential-sync/package.json index fd455c040994df..cadaad94f2ffdb 100644 --- a/example-apps/credential-sync/package.json +++ b/example-apps/credential-sync/package.json @@ -5,8 +5,7 @@ "scripts": { "dev": "PORT=5100 next dev", "build": "next build", - "start": "next start", - "lint": "biome lint ." + "start": "next start" }, "dependencies": { "@calcom/atoms": "workspace:*", diff --git a/package.json b/package.json index 731198f72395b1..039bc9cf0e9e98 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "lint-staged": "lint-staged" }, "devDependencies": { - "@biomejs/biome": "^2.3.8", + "@biomejs/biome": "2.3.10", "@changesets/changelog-github": "0.5.1", "@changesets/cli": "2.29.4", "@faker-js/faker": "9.2.0", diff --git a/packages/coss-ui/package.json b/packages/coss-ui/package.json index 10383fece70825..dde1846e31847b 100644 --- a/packages/coss-ui/package.json +++ b/packages/coss-ui/package.json @@ -9,8 +9,8 @@ "./hooks/*": "./src/hooks/*.ts" }, "scripts": { - "lint": "biome lint .", - "lint:fix": "biome lint --write .", + "lint": "biome lint src", + "lint:fix": "biome lint --write src", "type-check": "tsc --pretty --noEmit", "type-check:ci": "tsc-absolute --pretty --noEmit", "registry:pull": "ts-node ../../scripts/pull-coss-ui-components.ts" @@ -27,6 +27,7 @@ "react-dom": "^18.0.0 || ^19.0.0" }, "devDependencies": { + "@biomejs/biome": "2.3.10", "ts-node": "10.9.2", "tsc-absolute": "1.0.0", "typescript": "5.9.0-beta" diff --git a/packages/embeds/embed-core/package.json b/packages/embeds/embed-core/package.json index eb095e31b20679..7bf5cb155004fa 100644 --- a/packages/embeds/embed-core/package.json +++ b/packages/embeds/embed-core/package.json @@ -41,6 +41,7 @@ "dist" ], "devDependencies": { + "@biomejs/biome": "2.3.10", "@playwright/test": "1.57.0", "@tailwindcss/cli": "4.1.16", "@tailwindcss/postcss": "4.1.15", diff --git a/packages/lib/package.json b/packages/lib/package.json index 5dd524dd0b821f..231b91cfbdd81a 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -30,6 +30,7 @@ "uuid": "8.3.2" }, "devDependencies": { + "@biomejs/biome": "2.3.10", "@calcom/tsconfig": "workspace:*", "@calcom/types": "workspace:*", "@faker-js/faker": "7.6.0", diff --git a/packages/platform/examples/base/package.json b/packages/platform/examples/base/package.json index 7df89d7abdd7de..1ebb7e027f97ec 100644 --- a/packages/platform/examples/base/package.json +++ b/packages/platform/examples/base/package.json @@ -7,7 +7,6 @@ "dev": "PORT=4321 next dev", "build": "next build", "start": "next start", - "lint": "biome lint .", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui" }, diff --git a/packages/trpc/package.json b/packages/trpc/package.json index 1c0f8c7277675c..8d1aa7664fea10 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -30,6 +30,7 @@ "react-dom": "^18.0.0 || ^19.0.0" }, "devDependencies": { + "@biomejs/biome": "2.3.10", "@tanstack/react-query": "5.17.19", "@types/cookie": "0.6.0", "@types/uuid": "8.3.4", diff --git a/packages/ui/components/icon/icon-names.ts b/packages/ui/components/icon/icon-names.ts index c6a977ea4e9cca..d4798ce3bdeb8a 100644 --- a/packages/ui/components/icon/icon-names.ts +++ b/packages/ui/components/icon/icon-names.ts @@ -2,158 +2,158 @@ export type IconName = | "activity" - | "arrow-down" - | "arrow-left" - | "arrow-right" - | "arrow-up-right" - | "arrow-up" - | "asterisk" - | "at-sign" - | "atom" - | "badge-check" - | "ban" - | "bell" - | "binary" - | "blocks" - | "bold" - | "book-open-check" - | "book-open" - | "book-user" - | "book" - | "bookmark" - | "building" - | "calendar-check-2" - | "calendar-days" - | "calendar-heart" - | "calendar-range" - | "calendar-search" - | "calendar-x-2" - | "calendar" - | "chart-bar" - | "chart-line" - | "check-check" - | "check" - | "chevron-down" - | "chevron-left" - | "chevron-right" - | "chevron-up" - | "chevrons-down-up" - | "chevrons-left" - | "chevrons-right" - | "chevrons-up-down" - | "circle-alert" - | "circle-arrow-up" - | "circle-check-big" - | "circle-check" - | "circle-help" - | "circle-plus" - | "circle-x" - | "circle" - | "clipboard-check" - | "clipboard" - | "clock" - | "code" - | "columns-3" - | "command" - | "contact" - | "copy" - | "corner-down-left" - | "corner-down-right" - | "credit-card" - | "disc" - | "dot" - | "download" - | "ellipsis-vertical" - | "ellipsis" - | "external-link" - | "eye-off" - | "eye" - | "file-down" - | "file-text" - | "file" - | "filter" - | "fingerprint" - | "flag" - | "folder" - | "gift" - | "git-merge" - | "github" - | "globe" - | "grid-3x3" - | "handshake" - | "info" - | "italic" - | "key" - | "layers" - | "layout-dashboard" - | "link-2" - | "link" - | "list-filter" - | "loader" - | "lock-open" - | "lock" - | "log-out" - | "mail-open" - | "mail" - | "map-pin" - | "map" - | "menu" - | "message-circle" - | "messages-square" - | "mic-off" - | "mic" - | "monitor" - | "moon" - | "paintbrush" - | "paperclip" - | "pause" - | "pencil" - | "phone-call" - | "phone-incoming" - | "phone-off" - | "phone-outgoing" - | "phone" - | "play" - | "plus" - | "refresh-ccw" - | "refresh-cw" - | "repeat" - | "rocket" - | "rotate-ccw" - | "rotate-cw" - | "search" - | "send" - | "settings" - | "share-2" - | "shield-check" - | "shield" - | "shuffle" - | "sliders-horizontal" - | "sliders-vertical" - | "smartphone" - | "sparkles" - | "split" - | "square-check" - | "square-pen" - | "star" - | "sun" - | "sunrise" - | "sunset" - | "tags" - | "terminal" - | "trash-2" - | "trash" - | "trello" - | "triangle-alert" - | "upload" - | "user-check" - | "user-plus" - | "user-x" - | "user" - | "users" - | "venetian-mask" - | "video" - | "waypoints" - | "webhook" - | "x" - | "zap"; + | "arrow-down" + | "arrow-left" + | "arrow-right" + | "arrow-up-right" + | "arrow-up" + | "asterisk" + | "at-sign" + | "atom" + | "badge-check" + | "ban" + | "bell" + | "binary" + | "blocks" + | "bold" + | "book-open-check" + | "book-open" + | "book-user" + | "book" + | "bookmark" + | "building" + | "calendar-check-2" + | "calendar-days" + | "calendar-heart" + | "calendar-range" + | "calendar-search" + | "calendar-x-2" + | "calendar" + | "chart-bar" + | "chart-line" + | "check-check" + | "check" + | "chevron-down" + | "chevron-left" + | "chevron-right" + | "chevron-up" + | "chevrons-down-up" + | "chevrons-left" + | "chevrons-right" + | "chevrons-up-down" + | "circle-alert" + | "circle-arrow-up" + | "circle-check-big" + | "circle-check" + | "circle-help" + | "circle-plus" + | "circle-x" + | "circle" + | "clipboard-check" + | "clipboard" + | "clock" + | "code" + | "columns-3" + | "command" + | "contact" + | "copy" + | "corner-down-left" + | "corner-down-right" + | "credit-card" + | "disc" + | "dot" + | "download" + | "ellipsis-vertical" + | "ellipsis" + | "external-link" + | "eye-off" + | "eye" + | "file-down" + | "file-text" + | "file" + | "filter" + | "fingerprint" + | "flag" + | "folder" + | "gift" + | "git-merge" + | "github" + | "globe" + | "grid-3x3" + | "handshake" + | "info" + | "italic" + | "key" + | "layers" + | "layout-dashboard" + | "link-2" + | "link" + | "list-filter" + | "loader" + | "lock-open" + | "lock" + | "log-out" + | "mail-open" + | "mail" + | "map-pin" + | "map" + | "menu" + | "message-circle" + | "messages-square" + | "mic-off" + | "mic" + | "monitor" + | "moon" + | "paintbrush" + | "paperclip" + | "pause" + | "pencil" + | "phone-call" + | "phone-incoming" + | "phone-off" + | "phone-outgoing" + | "phone" + | "play" + | "plus" + | "refresh-ccw" + | "refresh-cw" + | "repeat" + | "rocket" + | "rotate-ccw" + | "rotate-cw" + | "search" + | "send" + | "settings" + | "share-2" + | "shield-check" + | "shield" + | "shuffle" + | "sliders-horizontal" + | "sliders-vertical" + | "smartphone" + | "sparkles" + | "split" + | "square-check" + | "square-pen" + | "star" + | "sun" + | "sunrise" + | "sunset" + | "tags" + | "terminal" + | "trash-2" + | "trash" + | "trello" + | "triangle-alert" + | "upload" + | "user-check" + | "user-plus" + | "user-x" + | "user" + | "users" + | "venetian-mask" + | "video" + | "waypoints" + | "webhook" + | "x" + | "zap"; diff --git a/packages/ui/scripts/build-icons.mjs b/packages/ui/scripts/build-icons.mjs index 3142fd9427b98d..19f07730747fb4 100644 --- a/packages/ui/scripts/build-icons.mjs +++ b/packages/ui/scripts/build-icons.mjs @@ -32,13 +32,21 @@ async function generateIconFiles() { const spriteFilepath = path.join(outputDir, "sprite.svg"); const typeOutputFilepath = path.join(typesDir, "icon-names.ts"); - const currentSprite = await fsExtra.readFile(spriteFilepath, "utf8").catch(() => ""); - const currentTypes = await fsExtra.readFile(typeOutputFilepath, "utf8").catch(() => ""); + const currentSprite = await fsExtra + .readFile(spriteFilepath, "utf8") + .catch(() => ""); + const currentTypes = await fsExtra + .readFile(typeOutputFilepath, "utf8") + .catch(() => ""); const iconNames = files.map((file) => iconName(file)); - const spriteUpToDate = iconNames.every((name) => currentSprite.includes(`id=${name}`)); - const typesUpToDate = iconNames.every((name) => currentTypes.includes(`"${name}"`)); + const spriteUpToDate = iconNames.every((name) => + currentSprite.includes(`id=${name}`) + ); + const typesUpToDate = iconNames.every((name) => + currentTypes.includes(`"${name}"`) + ); if (spriteUpToDate && typesUpToDate) { logVerbose(`Icons are up to date`); @@ -63,9 +71,12 @@ async function generateIconFiles() { const typeOutputContent = `// This file is generated by yarn run build:icons export type IconName = -\t| ${stringifiedIconNames.join("\n\t| ")}; +\t| ${stringifiedIconNames.join("\n | ")}; `; - const typesChanged = await writeIfChanged(typeOutputFilepath, typeOutputContent); + const typesChanged = await writeIfChanged( + typeOutputFilepath, + typeOutputContent + ); logVerbose(`Manifest saved to ${path.relative(cwd, typeOutputFilepath)}`); @@ -119,7 +130,9 @@ async function generateSvgSprite({ files, inputDir, outputPath }) { } async function writeIfChanged(filepath, newContent) { - const currentContent = await fsExtra.readFile(filepath, "utf8").catch(() => ""); + const currentContent = await fsExtra + .readFile(filepath, "utf8") + .catch(() => ""); if (currentContent === newContent) return false; await fsExtra.writeFile(filepath, newContent, "utf8"); await $`node ${biomeBin} format --write ${filepath}`; diff --git a/yarn.lock b/yarn.lock index be3a74d4af88e1..268751c61d3124 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1605,7 +1605,7 @@ __metadata: languageName: node linkType: hard -"@biomejs/biome@npm:^2.3.8": +"@biomejs/biome@npm:2.3.10, @biomejs/biome@npm:^2.3.8": version: 2.3.10 resolution: "@biomejs/biome@npm:2.3.10" dependencies: @@ -2376,6 +2376,7 @@ __metadata: version: 0.0.0-use.local resolution: "@calcom/embed-core@workspace:packages/embeds/embed-core" dependencies: + "@biomejs/biome": "npm:2.3.10" "@playwright/test": "npm:1.57.0" "@tailwindcss/cli": "npm:4.1.16" "@tailwindcss/postcss": "npm:4.1.15" @@ -2767,6 +2768,7 @@ __metadata: version: 0.0.0-use.local resolution: "@calcom/lib@workspace:packages/lib" dependencies: + "@biomejs/biome": "npm:2.3.10" "@calcom/config": "workspace:*" "@calcom/dayjs": "workspace:*" "@calcom/tsconfig": "workspace:*" @@ -3251,6 +3253,7 @@ __metadata: version: 0.0.0-use.local resolution: "@calcom/trpc@workspace:packages/trpc" dependencies: + "@biomejs/biome": "npm:2.3.10" "@tanstack/react-query": "npm:5.17.19" "@trpc/client": "npm:11.0.0-next-beta.222" "@trpc/next": "npm:11.0.0-next-beta.222" @@ -3403,6 +3406,7 @@ __metadata: resolution: "@calcom/web@workspace:apps/web" dependencies: "@babel/core": "npm:7.26.10" + "@biomejs/biome": "npm:2.3.10" "@boxyhq/saml-jackson": "npm:1.52.2" "@calcom/app-store": "workspace:*" "@calcom/app-store-cli": "workspace:*" @@ -3980,6 +3984,7 @@ __metadata: resolution: "@coss/ui@workspace:packages/coss-ui" dependencies: "@base-ui/react": "npm:1.0.0" + "@biomejs/biome": "npm:2.3.10" class-variance-authority: "npm:0.7.1" clsx: "npm:2.1.1" lucide-react: "npm:0.555.0" @@ -18758,7 +18763,7 @@ __metadata: version: 0.0.0-use.local resolution: "calcom-monorepo@workspace:." dependencies: - "@biomejs/biome": "npm:^2.3.8" + "@biomejs/biome": "npm:2.3.10" "@changesets/changelog-github": "npm:0.5.1" "@changesets/cli": "npm:2.29.4" "@faker-js/faker": "npm:9.2.0" From 2c7cb530a11109beb2bd840f2d00f673644afa97 Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Tue, 30 Dec 2025 19:03:38 +0530 Subject: [PATCH 02/11] fix: add eventTypeId and eventTypeSlug guard (#25773) * fix: add eventTypeId and eventTypeSlug guard * chore: update test * refactor: improvemnt * revert: --------- Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> --- apps/api/v1/test/lib/bookings/_post.test.ts | 4 +++- .../features/bookings/lib/handleNewBooking/getEventType.ts | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/api/v1/test/lib/bookings/_post.test.ts b/apps/api/v1/test/lib/bookings/_post.test.ts index 3e397eefc28019..de13aeb627b71e 100644 --- a/apps/api/v1/test/lib/bookings/_post.test.ts +++ b/apps/api/v1/test/lib/bookings/_post.test.ts @@ -245,7 +245,9 @@ describe("POST /api/bookings", () => { const { req, res } = createMocks({ method: "POST", - body: {}, + body: { + eventTypeId: 2, + }, }); await handler(req, res); diff --git a/packages/features/bookings/lib/handleNewBooking/getEventType.ts b/packages/features/bookings/lib/handleNewBooking/getEventType.ts index bb5313375b3994..ba0fa32b8c9df1 100644 --- a/packages/features/bookings/lib/handleNewBooking/getEventType.ts +++ b/packages/features/bookings/lib/handleNewBooking/getEventType.ts @@ -1,4 +1,5 @@ import { getDefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents"; +import { HttpError } from "@calcom/lib/http-error"; import { withReporting } from "@calcom/lib/sentryWrapper"; import { getBookingFieldsWithSystemFields } from "../getBookingFields"; @@ -11,6 +12,10 @@ const _getEventType = async ({ eventTypeId: number; eventTypeSlug?: string; }) => { + if (!eventTypeId && !eventTypeSlug) { + throw new HttpError({ statusCode: 400, message: "Either eventTypeId or eventTypeSlug must be provided" }); + } + // handle dynamic user const eventType = !eventTypeId && !!eventTypeSlug ? getDefaultEvent(eventTypeSlug) : await getEventTypesFromDB(eventTypeId); From cc8da034c092950affbb7417ba6b920249f5dae9 Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Tue, 30 Dec 2025 19:15:01 +0530 Subject: [PATCH 03/11] test: flaky e2e tests (#26308) * fix another * fix flakes * Update apps/web/playwright/fixtures/apps.ts Co-authored-by: Keith Williams * fix * fix * test fix * fix test * tweak --------- Co-authored-by: Keith Williams --- apps/web/playwright/booking-pages.e2e.ts | 284 +++++++++++++----- apps/web/playwright/fixtures/apps.ts | 4 +- .../web/playwright/managed-event-types.e2e.ts | 2 +- apps/web/playwright/payment-apps.e2e.ts | 2 +- 4 files changed, 219 insertions(+), 73 deletions(-) diff --git a/apps/web/playwright/booking-pages.e2e.ts b/apps/web/playwright/booking-pages.e2e.ts index c0f598bd8475ca..f38e672c5020ca 100644 --- a/apps/web/playwright/booking-pages.e2e.ts +++ b/apps/web/playwright/booking-pages.e2e.ts @@ -1,12 +1,3 @@ -import { expect } from "@playwright/test"; -import { JSDOM } from "jsdom"; - -import { WEBAPP_URL } from "@calcom/lib/constants"; -import { generateHashedLink } from "@calcom/lib/generateHashedLink"; -import { randomString } from "@calcom/lib/random"; -import { SchedulingType } from "@calcom/prisma/enums"; -import type { Schedule, TimeRange } from "@calcom/types/schedule"; - import { test, todo } from "./lib/fixtures"; import { bookFirstEvent, @@ -20,6 +11,13 @@ import { testName, cancelBookingFromBookingsList, } from "./lib/testUtils"; +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { generateHashedLink } from "@calcom/lib/generateHashedLink"; +import { randomString } from "@calcom/lib/random"; +import { SchedulingType } from "@calcom/prisma/enums"; +import type { Schedule, TimeRange } from "@calcom/types/schedule"; +import { expect } from "@playwright/test"; +import { JSDOM } from "jsdom"; const freeUserObj = { name: `Free-user-${randomString(3)}` }; test.describe.configure({ mode: "parallel" }); @@ -33,7 +31,9 @@ test("check SSR and OG - User Event Type", async ({ page, users }) => { name, }); const responsePromise = page.waitForResponse( - (response) => response.url().includes(`/${user.username}/30-min`) && response.status() === 200 + (response) => + response.url().includes(`/${user.username}/30-min`) && + response.status() === 200 ); await page.goto(`/${user.username}/30-min`); await page.content(); @@ -42,17 +42,27 @@ test("check SSR and OG - User Event Type", async ({ page, users }) => { const document = new JSDOM(ssrResponse).window.document; const titleText = document.querySelector("title")?.textContent; - const ogImage = document.querySelector('meta[property="og:image"]')?.getAttribute("content"); - const ogUrl = document.querySelector('meta[property="og:url"]')?.getAttribute("content"); - const canonicalLink = document.querySelector('link[rel="canonical"]')?.getAttribute("href"); + const ogImage = document + .querySelector('meta[property="og:image"]') + ?.getAttribute("content"); + const ogUrl = document + .querySelector('meta[property="og:url"]') + ?.getAttribute("content"); + const canonicalLink = document + .querySelector('link[rel="canonical"]') + ?.getAttribute("href"); expect(titleText).toContain(name); expect(ogUrl).toEqual(`${WEBAPP_URL}/${user.username}/30-min`); await page.waitForSelector('[data-testid="avatar-href"]'); - const avatarLocators = await page.locator('[data-testid="avatar-href"]').all(); + const avatarLocators = await page + .locator('[data-testid="avatar-href"]') + .all(); expect(avatarLocators.length).toBe(1); for (const avatarLocator of avatarLocators) { - expect(await avatarLocator.getAttribute("href")).toEqual(`${WEBAPP_URL}/${user.username}?redirect=false`); + expect(await avatarLocator.getAttribute("href")).toEqual( + `${WEBAPP_URL}/${user.username}?redirect=false` + ); } expect(canonicalLink).toEqual(`${WEBAPP_URL}/${user.username}/30-min`); @@ -157,7 +167,10 @@ test.describe("pro user", () => { await page.goto("/bookings/upcoming"); await page.waitForSelector('[data-testid="bookings"]'); // Click the ellipsis menu button to open the dropdown - await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click(); + await page + .locator('[data-testid="booking-actions-dropdown"]') + .nth(0) + .click(); await page.locator('[data-testid="reschedule"]').click(); await page.waitForURL((url) => { const bookingId = url.searchParams.get("rescheduleUid"); @@ -178,17 +191,31 @@ test.describe("pro user", () => { }) => { const [pro] = users.get(); const [eventType] = pro.eventTypes; - const bookingFixture = await bookings.create(pro.id, pro.username, eventType.id); + const bookingFixture = await bookings.create( + pro.id, + pro.username, + eventType.id + ); // open the wrong eventType (rescheduleUid created for /30min event) - await page.goto(`${pro.username}/${pro.eventTypes[1].slug}?rescheduleUid=${bookingFixture.uid}`); + await page.goto( + `${pro.username}/${pro.eventTypes[1].slug}?rescheduleUid=${bookingFixture.uid}` + ); - await expect(page).toHaveURL(new RegExp(`${pro.username}/${eventType.slug}`)); + await expect(page).toHaveURL( + new RegExp(`${pro.username}/${eventType.slug}`) + ); }); - test("it returns a 404 when a requested event type does not exist", async ({ page, users }) => { + test("it returns a 404 when a requested event type does not exist", async ({ + page, + users, + }) => { const [pro] = users.get(); - const unexistingPageUrl = new URL(`${pro.username}/invalid-event-type`, WEBAPP_URL); + const unexistingPageUrl = new URL( + `${pro.username}/invalid-event-type`, + WEBAPP_URL + ); const response = await page.goto(unexistingPageUrl.href); expect(response?.status()).toBe(404); }); @@ -200,8 +227,12 @@ test.describe("pro user", () => { // Because it tests the entire booking flow + the cancellation + rebooking test.setTimeout(testInfo.timeout * 3); await bookFirstEvent(page); - await expect(page.locator(`[data-testid="attendee-email-${testEmail}"]`)).toHaveText(testEmail); - await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName); + await expect( + page.locator(`[data-testid="attendee-email-${testEmail}"]`) + ).toHaveText(testEmail); + await expect( + page.locator(`[data-testid="attendee-name-${testName}"]`) + ).toHaveText(testName); const [pro] = users.get(); await pro.apiLogin(); @@ -224,8 +255,12 @@ test.describe("pro user", () => { // Because it tests the entire booking flow + the cancellation + rebooking test.setTimeout(testInfo.timeout * 3); await bookFirstEvent(page); - await expect(page.locator(`[data-testid="attendee-email-${testEmail}"]`)).toHaveText(testEmail); - await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName); + await expect( + page.locator(`[data-testid="attendee-email-${testEmail}"]`) + ).toHaveText(testEmail); + await expect( + page.locator(`[data-testid="attendee-name-${testName}"]`) + ).toHaveText(testName); const [pro] = users.get(); await pro.apiLogin(); @@ -242,7 +277,9 @@ test.describe("pro user", () => { await page.goto(`/reschedule/${bookingUid}`); expect(page.url()).not.toContain("rescheduleUid"); - const cancelledHeadline = page.locator('[data-testid="cancelled-headline"]'); + const cancelledHeadline = page.locator( + '[data-testid="cancelled-headline"]' + ); await expect(cancelledHeadline).toBeVisible(); }); @@ -257,14 +294,18 @@ test.describe("pro user", () => { await page.goto("/bookings/unconfirmed"); await Promise.all([ page.click('[data-testid="confirm"]'), - page.waitForResponse((response) => response.url().includes("/api/trpc/bookings/confirm")), + page.waitForResponse((response) => + response.url().includes("/api/trpc/bookings/confirm") + ), ]); // This is the only booking in there that needed confirmation and now it should be empty screen await expect(page.locator('[data-testid="empty-screen"]')).toBeVisible(); }); test("can book an unconfirmed event multiple times", async ({ page }) => { - await page.locator('[data-testid="event-type-link"]:has-text("Opt in")').click(); + await page + .locator('[data-testid="event-type-link"]:has-text("Opt in")') + .click(); await selectFirstAvailableTimeSlotNextMonth(page); const pageUrl = page.url(); @@ -281,7 +322,9 @@ test.describe("pro user", () => { test("booking an unconfirmed event with the same email brings you to the original request", async ({ page, }) => { - await page.locator('[data-testid="event-type-link"]:has-text("Opt in")').click(); + await page + .locator('[data-testid="event-type-link"]:has-text("Opt in")') + .click(); await selectFirstAvailableTimeSlotNextMonth(page); const pageUrl = page.url(); @@ -312,12 +355,17 @@ test.describe("pro user", () => { await expect(page.locator("[data-testid=success-page]")).toBeVisible(); const promises = additionalGuests.map(async (email) => { - await expect(page.locator(`[data-testid="attendee-email-${email}"]`)).toHaveText(email); + await expect( + page.locator(`[data-testid="attendee-email-${email}"]`) + ).toHaveText(email); }); await Promise.all(promises); }); - test("Time slots should be reserved when selected", async ({ page, browser }) => { + test("Time slots should be reserved when selected", async ({ + page, + browser, + }) => { const initialUrl = page.url(); await page.locator('[data-testid="event-type-link"]').first().click(); await selectFirstAvailableTimeSlotNextMonth(page); @@ -325,16 +373,29 @@ test.describe("pro user", () => { const pageTwoInNewContext = await newContext.newPage(); await pageTwoInNewContext.goto(initialUrl); await pageTwoInNewContext.waitForURL(initialUrl); - await pageTwoInNewContext.locator('[data-testid="event-type-link"]').first().click(); - - await pageTwoInNewContext.locator('[data-testid="incrementMonth"]').waitFor(); + await pageTwoInNewContext + .locator('[data-testid="event-type-link"]') + .first() + .click(); + + await pageTwoInNewContext + .locator('[data-testid="incrementMonth"]') + .waitFor(); await pageTwoInNewContext.click('[data-testid="incrementMonth"]'); - await pageTwoInNewContext.locator('[data-testid="day"][data-disabled="false"]').nth(0).waitFor(); - await pageTwoInNewContext.locator('[data-testid="day"][data-disabled="false"]').nth(0).click(); + await pageTwoInNewContext + .locator('[data-testid="day"][data-disabled="false"]') + .nth(0) + .waitFor(); + await pageTwoInNewContext + .locator('[data-testid="day"][data-disabled="false"]') + .nth(0) + .click(); // 9:30 should be the first available time slot await pageTwoInNewContext.locator('[data-testid="time"]').nth(0).waitFor(); - const firstSlotAvailable = pageTwoInNewContext.locator('[data-testid="time"]').nth(0); + const firstSlotAvailable = pageTwoInNewContext + .locator('[data-testid="time"]') + .nth(0); // Find text inside the element const firstSlotAvailableText = await firstSlotAvailable.innerText(); expect(firstSlotAvailableText).toContain("9:30"); @@ -346,7 +407,9 @@ test.describe("pro user", () => { }) => { const initialUrl = page.url(); await page.waitForSelector('[data-testid="event-type-link"]'); - const eventTypeLink = page.locator('[data-testid="event-type-link"]').first(); + const eventTypeLink = page + .locator('[data-testid="event-type-link"]') + .first(); await eventTypeLink.click(); await selectFirstAvailableTimeSlotNextMonth(page); @@ -355,7 +418,9 @@ test.describe("pro user", () => { await pageTwo.waitForURL(initialUrl); await pageTwo.waitForSelector('[data-testid="event-type-link"]'); - const eventTypeLinkTwo = pageTwo.locator('[data-testid="event-type-link"]').first(); + const eventTypeLinkTwo = pageTwo + .locator('[data-testid="event-type-link"]') + .first(); await eventTypeLinkTwo.waitFor(); await eventTypeLinkTwo.click(); @@ -364,8 +429,14 @@ test.describe("pro user", () => { await pageTwo.locator('[data-testid="incrementMonth"]').waitFor(); await pageTwo.click('[data-testid="incrementMonth"]'); - await pageTwo.locator('[data-testid="day"][data-disabled="false"]').nth(0).waitFor(); - await pageTwo.locator('[data-testid="day"][data-disabled="false"]').nth(0).click(); + await pageTwo + .locator('[data-testid="day"][data-disabled="false"]') + .nth(0) + .waitFor(); + await pageTwo + .locator('[data-testid="day"][data-disabled="false"]') + .nth(0) + .click(); await pageTwo.locator('[data-testid="time"]').nth(0).waitFor(); const firstSlotAvailable = pageTwo.locator('[data-testid="time"]').nth(0); @@ -384,7 +455,9 @@ test.describe("prefill", () => { await test.step("from session", async () => { await selectFirstAvailableTimeSlotNextMonth(page); - await expect(page.locator('[name="name"]')).toHaveValue(prefill.name || ""); + await expect(page.locator('[name="name"]')).toHaveValue( + prefill.name || "" + ); await expect(page.locator('[name="email"]')).toHaveValue(prefill.email); }); @@ -399,7 +472,9 @@ test.describe("prefill", () => { }); }); - test("Persist the field values when going back and coming back to the booking form", async ({ page }) => { + test("Persist the field values when going back and coming back to the booking form", async ({ + page, + }) => { await page.goto("/pro/30min"); await selectFirstAvailableTimeSlotNextMonth(page); await page.fill('[name="name"]', "John Doe"); @@ -409,7 +484,9 @@ test.describe("prefill", () => { await selectFirstAvailableTimeSlotNextMonth(page); await expect(page.locator('[name="name"]')).toHaveValue("John Doe"); - await expect(page.locator('[name="email"]')).toHaveValue("john@example.com"); + await expect(page.locator('[name="email"]')).toHaveValue( + "john@example.com" + ); await expect(page.locator('[name="notes"]')).toHaveValue("Test notes"); }); @@ -429,7 +506,9 @@ test.describe("prefill", () => { }); }); - test("skip confirm step if all fields are prefilled from query params", async ({ page }) => { + test("skip confirm step if all fields are prefilled from query params", async ({ + page, + }) => { await page.goto("/pro/30min"); const url = new URL(page.url()); url.searchParams.set("name", testName); @@ -440,7 +519,9 @@ test.describe("prefill", () => { await page.goto(url.toString()); await selectFirstAvailableTimeSlotNextMonth(page); - await expect(page.locator('[data-testid="skip-confirm-book-button"]')).toBeVisible(); + await expect( + page.locator('[data-testid="skip-confirm-book-button"]') + ).toBeVisible(); await page.click('[data-testid="skip-confirm-book-button"]'); await expect(page.locator("[data-testid=success-page]")).toBeVisible(); @@ -455,7 +536,15 @@ test.describe("Booking on different layouts", () => { start: new Date(new Date().setUTCHours(9, 0, 0, 0)), end: new Date(new Date().setUTCHours(17, 0, 0, 0)), }; - const schedule: Schedule = [[], [dateRanges], [dateRanges], [dateRanges], [dateRanges], [dateRanges], []]; + const schedule: Schedule = [ + [], + [dateRanges], + [dateRanges], + [dateRanges], + [dateRanges], + [dateRanges], + [], + ]; const user = await users.create({ schedule }); await page.goto(`/${user.username}`); @@ -490,6 +579,13 @@ test.describe("Booking on different layouts", () => { await page.click('[data-testid="incrementMonth"]'); + await page.waitForURL((url) => { + return url.searchParams.has("month"); + }) + + await page.reload(); + await page.waitForLoadState("networkidle"); + await page.locator('[data-testid="time"]').nth(1).click(); // Fill what is this meeting about? name email and notes @@ -513,7 +609,15 @@ test.describe("Booking round robin event", () => { end: new Date(new Date().setUTCHours(17, 0, 0, 0)), }; - const schedule: Schedule = [[], [dateRanges], [dateRanges], [dateRanges], [dateRanges], [dateRanges], []]; + const schedule: Schedule = [ + [], + [dateRanges], + [dateRanges], + [dateRanges], + [dateRanges], + [dateRanges], + [], + ]; const testUser = await users.create( { schedule }, @@ -621,12 +725,16 @@ test.describe("Event type with disabled cancellation and rescheduling", () => { bookingId = pathSegments[pathSegments.length - 1]; }); - test("Reschedule and cancel buttons should be hidden on success page", async ({ page }) => { + test("Reschedule and cancel buttons should be hidden on success page", async ({ + page, + }) => { await expect(page.locator('[data-testid="reschedule-link"]')).toBeHidden(); await expect(page.locator('[data-testid="cancel"]')).toBeHidden(); }); - test("Direct access to reschedule/{bookingId} should redirect to success page", async ({ page }) => { + test("Direct access to reschedule/{bookingId} should redirect to success page", async ({ + page, + }) => { await page.goto(`/reschedule/${bookingId}`); await expect(page.locator("[data-testid=success-page]")).toBeVisible(); @@ -634,15 +742,21 @@ test.describe("Event type with disabled cancellation and rescheduling", () => { await page.waitForURL((url) => url.pathname === `/booking/${bookingId}`); }); - test("Using rescheduleUid query parameter should redirect to success page", async ({ page }) => { - await page.goto(`/${user.username}/no-cancel-no-reschedule?rescheduleUid=${bookingId}`); + test("Using rescheduleUid query parameter should redirect to success page", async ({ + page, + }) => { + await page.goto( + `/${user.username}/no-cancel-no-reschedule?rescheduleUid=${bookingId}` + ); await expect(page.locator("[data-testid=success-page]")).toBeVisible(); await page.waitForURL((url) => url.pathname === `/booking/${bookingId}`); }); - test("Should prevent cancellation and show an error message", async ({ page }) => { + test("Should prevent cancellation and show an error message", async ({ + page, + }) => { const csrfTokenResponse = await page.request.get("/api/csrf"); const { csrfToken } = await csrfTokenResponse.json(); const response = await page.request.post("/api/cancel", { @@ -657,10 +771,15 @@ test.describe("Event type with disabled cancellation and rescheduling", () => { expect(response.status()).toBe(400); const responseBody = await response.json(); - expect(responseBody.message).toBe("This event type does not allow cancellations"); + expect(responseBody.message).toBe( + "This event type does not allow cancellations" + ); }); }); -test("Should throw error when both seatsPerTimeSlot and recurringEvent are set", async ({ page, users }) => { +test("Should throw error when both seatsPerTimeSlot and recurringEvent are set", async ({ + page, + users, +}) => { const user = await users.create({ name: `Test-user-${randomString(4)}`, eventTypes: [ @@ -699,7 +818,11 @@ test.describe("GTM container", () => { await users.create(); }); - test("global GTM should not be loaded on private booking link", async ({ page, users, prisma }) => { + test("global GTM should not be loaded on private booking link", async ({ + page, + users, + prisma, + }) => { const [user] = users.get(); const eventType = await user.getFirstEventAsOwner(); @@ -722,9 +845,12 @@ test.describe("GTM container", () => { }); const getScheduleRespPromise = page.waitForResponse( - (response) => response.url().includes("getSchedule") && response.status() === 200 + (response) => + response.url().includes("getSchedule") && response.status() === 200 + ); + await page.goto( + `/d/${eventWithPrivateLink.hashedLink[0]?.link}/${eventWithPrivateLink.slug}` ); - await page.goto(`/d/${eventWithPrivateLink.hashedLink[0]?.link}/${eventWithPrivateLink.slug}`); await page.waitForLoadState("domcontentloaded"); await getScheduleRespPromise; @@ -732,15 +858,23 @@ test.describe("GTM container", () => { await expect(injectedScript).not.toBeAttached(); }); - test("global GTM should be loaded on non-booking pages", async ({ page, users }) => { - test.skip(!process.env.NEXT_PUBLIC_BODY_SCRIPTS, "Skipping test as NEXT_PUBLIC_BODY_SCRIPTS is not set"); + test("global GTM should be loaded on non-booking pages", async ({ + page, + users, + }) => { + test.skip( + !process.env.NEXT_PUBLIC_BODY_SCRIPTS, + "Skipping test as NEXT_PUBLIC_BODY_SCRIPTS is not set" + ); const [user] = users.get(); await user.apiLogin(); // Go to /insights page and wait for one of the common API call to complete const eventsByStatusRespPromise = page.waitForResponse( - (response) => response.url().includes("getEventTypesFromGroup") && response.status() === 200 + (response) => + response.url().includes("getEventTypesFromGroup") && + response.status() === 200 ); await page.goto(`/insights`); await page.waitForLoadState("domcontentloaded"); @@ -755,7 +889,11 @@ test.describe("GTM container", () => { }); test.describe("Past booking cancellation", () => { - test("Cancel button should be hidden for past bookings", async ({ page, users, bookings }) => { + test("Cancel button should be hidden for past bookings", async ({ + page, + users, + bookings, + }) => { const user = await users.create({ name: "Test User", }); @@ -766,15 +904,23 @@ test.describe("Past booking cancellation", () => { pastDate.setDate(pastDate.getDate() - 1); const endDate = new Date(pastDate.getTime() + 30 * 60 * 1000); - const booking = await bookings.create(user.id, user.username, user.eventTypes[0].id, { - title: "Past Meeting", - startTime: pastDate, - endTime: endDate, - status: "ACCEPTED", - }); + const booking = await bookings.create( + user.id, + user.username, + user.eventTypes[0].id, + { + title: "Past Meeting", + startTime: pastDate, + endTime: endDate, + status: "ACCEPTED", + } + ); await page.goto("/bookings/past"); - await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click(); + await page + .locator('[data-testid="booking-actions-dropdown"]') + .nth(0) + .click(); await expect(page.locator('[data-testid="cancel"]')).toBeDisabled(); await page.goto(`/booking/${booking.uid}`); diff --git a/apps/web/playwright/fixtures/apps.ts b/apps/web/playwright/fixtures/apps.ts index e2c359b10522c1..e44f64909c0678 100644 --- a/apps/web/playwright/fixtures/apps.ts +++ b/apps/web/playwright/fixtures/apps.ts @@ -117,7 +117,7 @@ export function createAppsFixture(page: Page) { }, goToAppsTab: async () => { await page.getByTestId("vertical-tab-apps").click(); - await expect(page.getByTestId("vertical-tab-apps")).toHaveAttribute("aria-current", "page"); + await expect(page.getByTestId("vertical-tab-apps").first()).toHaveAttribute("aria-current", "page"); }, activeApp: async (app: string) => { await page.locator(`[data-testid='${app}-app-switch']`).click(); @@ -127,7 +127,7 @@ export function createAppsFixture(page: Page) { }, verifyAppsInfoNew: async (app: string, eventTypeId: number) => { await page.goto(`event-types/${eventTypeId}?tabName=apps`); - await expect(page.getByTestId("vertical-tab-apps")).toHaveAttribute("aria-current", "page"); // fix the race condition + await expect(page.getByTestId("vertical-tab-apps").first()).toHaveAttribute("aria-current", "page"); // fix the race condition await expect(page.locator(`[data-testid='${app}-app-switch'][data-state="checked"]`)).toBeVisible(); }, }; diff --git a/apps/web/playwright/managed-event-types.e2e.ts b/apps/web/playwright/managed-event-types.e2e.ts index a9ee8b89121fd8..2e6898dabd02a8 100644 --- a/apps/web/playwright/managed-event-types.e2e.ts +++ b/apps/web/playwright/managed-event-types.e2e.ts @@ -34,7 +34,7 @@ test.describe("Managed Event Types", () => { // Let's create a team // Going to create an event type await page.goto("/event-types"); - const tabItem = page.getByTestId(`horizontal-tab-Owner`); + const tabItem = page.getByTestId("horizontal-tab-Owner").first(); await expect(tabItem).toBeVisible(); // We wait until loading is finished await page.waitForSelector('[data-testid="event-types"]'); diff --git a/apps/web/playwright/payment-apps.e2e.ts b/apps/web/playwright/payment-apps.e2e.ts index 2b5cf89b1e38ef..866a0a24b2d901 100644 --- a/apps/web/playwright/payment-apps.e2e.ts +++ b/apps/web/playwright/payment-apps.e2e.ts @@ -11,7 +11,7 @@ test.afterEach(({ users }) => users.deleteAll()); async function goToAppsTab(page: Page, eventTypeId?: number) { await page.goto(`event-types/${eventTypeId}?tabName=apps`); - await expect(page.getByTestId("vertical-tab-apps")).toHaveAttribute("aria-current", "page"); + await expect(page.getByTestId("vertical-tab-apps").first()).toHaveAttribute("aria-current", "page"); } test.describe("Payment app", () => { From 2b61bddf7bcb02d86da11302720e6582c2ae4d24 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Tue, 30 Dec 2025 08:57:49 -0500 Subject: [PATCH 04/11] feat: add completed onboarding column to organization members table (#25582) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> --- .../components/UserTable/UserListTable.tsx | 17 +++++++++++++++++ apps/web/public/static/locales/en/common.json | 1 + 2 files changed, 18 insertions(+) diff --git a/apps/web/modules/users/components/UserTable/UserListTable.tsx b/apps/web/modules/users/components/UserTable/UserListTable.tsx index 08365e2247ac3e..c70b2c6e25ecf2 100644 --- a/apps/web/modules/users/components/UserTable/UserListTable.tsx +++ b/apps/web/modules/users/components/UserTable/UserListTable.tsx @@ -79,6 +79,7 @@ const initalColumnVisibility = { teams: true, createdAt: false, updatedAt: false, + completedOnboarding: false, twoFactorEnabled: false, actions: true, }; @@ -418,6 +419,22 @@ function UserListTableContent({ }, cell: ({ row }) =>
{row.original.updatedAt || ""}
, }, + { + id: "completedOnboarding", + accessorKey: "completedOnboarding", + header: t("completed_onboarding"), + enableSorting: false, + enableColumnFilter: false, + size: 80, + cell: ({ row }) => { + const { completedOnboarding } = row.original; + return ( + + {completedOnboarding ? t("yes") : t("no")} + + ); + }, + }, { id: "twoFactorEnabled", accessorKey: "twoFactorEnabled", diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index d3a9eb82388c5f..b38ffb7ae53af1 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -3494,6 +3494,7 @@ "last_active": "Last Active", "member_since": "Member Since", "last_updated": "Last Updated", + "completed_onboarding": "Completed Onboarding", "salesforce_on_cancel_write_to_event": "On cancelled booking, write to event record instead of deleting event", "salesforce_on_every_cancellation": "On every cancellation", "report_issue": "Report issue", From 79857326839173740041371990c45859c534a12d Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Tue, 30 Dec 2025 11:24:33 -0300 Subject: [PATCH 05/11] chore: Delete cache-build cache entries on PR close (#26312) * chore: Delete cache-build cache entries on PR close Co-Authored-By: keith@cal.com * refactor: Extract cache-build key generation into reusable action - Create .github/actions/cache-build-key to generate cache keys - Update cache-build action to use the shared key action - Update delete workflow to use the shared key action - Checkout PR head SHA in delete workflow for correct hash computation Co-Authored-By: keith@cal.com * fix: Remove PR head SHA checkout per review feedback Co-Authored-By: keith@cal.com * refactor: Use prefix-based cache deletion for simpler cleanup - Use useblacksmith/cache-delete prefix mode to delete all caches matching branch - Remove dependency on cache-build-key action for deletion - No checkout needed since we're just using the branch name as prefix Co-Authored-By: keith@cal.com * refactor: Simplify cache key by removing Linux and node version segments Co-Authored-By: keith@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/actions/cache-build-key/action.yml | 23 +++++++++++++++++++ .github/actions/cache-build/action.yml | 20 +++++----------- .github/workflows/delete-blacksmith-cache.yml | 20 ++++++++++++++-- 3 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 .github/actions/cache-build-key/action.yml diff --git a/.github/actions/cache-build-key/action.yml b/.github/actions/cache-build-key/action.yml new file mode 100644 index 00000000000000..847989e46a70b4 --- /dev/null +++ b/.github/actions/cache-build-key/action.yml @@ -0,0 +1,23 @@ +name: Generate cache-build key +description: "Generate the cache key for production build caching" +inputs: + branch_key: + required: true + description: "Branch key for cache scoping (e.g., github.head_ref for PRs)" +outputs: + key: + description: "The generated cache key" + value: ${{ steps.generate-key.outputs.key }} +runs: + using: "composite" + steps: + - name: Generate cache key + id: generate-key + shell: bash + env: + CACHE_NAME: prod-build + BRANCH_KEY: ${{ inputs.branch_key }} + LOCKFILE_HASH: ${{ hashFiles('yarn.lock') }} + SOURCE_HASH: ${{ hashFiles('apps/**/**.[jt]s', 'apps/**/**.[jt]sx', 'apps/**/*.json', 'apps/**/*.css', 'packages/**/**.[jt]s', 'packages/**/**.[jt]sx', 'packages/prisma/schema.prisma', 'packages/prisma/migrations/**/*.sql', '!**/node_modules/**', '!packages/prisma/generated/**', '!packages/prisma/client/**', '!packages/prisma/zod/**', '!packages/kysely/**') }} + run: | + echo "key=${CACHE_NAME}-${BRANCH_KEY}-${LOCKFILE_HASH}-${SOURCE_HASH}" >> $GITHUB_OUTPUT diff --git a/.github/actions/cache-build/action.yml b/.github/actions/cache-build/action.yml index d3445a489ac7bd..2197e923705a9e 100644 --- a/.github/actions/cache-build/action.yml +++ b/.github/actions/cache-build/action.yml @@ -1,31 +1,23 @@ name: Cache production build binaries description: "Cache or restore if necessary" -inputs: - node_version: - required: false - default: v18.x runs: using: "composite" steps: + - name: Generate cache key + id: cache-key + uses: ./.github/actions/cache-build-key + with: + branch_key: ${{ github.head_ref || github.ref_name }} - name: Cache production build uses: actions/cache@v4 id: cache-build - env: - cache-name: prod-build - # Use branch name for cache scoping (head_ref for PRs, ref_name for pushes) - branch-key: ${{ github.head_ref || github.ref_name }} - # Hash yarn.lock for dependency changes - lockfile-hash: ${{ hashFiles('yarn.lock') }} - # Hash source files that affect the web build, excluding generated directories - # Excludes packages/prisma/{generated,client,zod}/* and packages/kysely/* which are generated by prisma generate - source-hash: ${{ hashFiles('apps/**/**.[jt]s', 'apps/**/**.[jt]sx', 'apps/**/*.json', 'apps/**/*.css', 'packages/**/**.[jt]s', 'packages/**/**.[jt]sx', 'packages/prisma/schema.prisma', 'packages/prisma/migrations/**/*.sql', '!**/node_modules/**', '!packages/prisma/generated/**', '!packages/prisma/client/**', '!packages/prisma/zod/**', '!packages/kysely/**') }} with: path: | ${{ github.workspace }}/apps/web/.next ${{ github.workspace }}/apps/web/public/embed **/.turbo/** **/dist/** - key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.node_version }}-${{ env.branch-key }}-${{ env.lockfile-hash }}-${{ env.source-hash }} + key: ${{ steps.cache-key.outputs.key }} - run: | export NODE_OPTIONS="--max_old_space_size=8192" yarn build diff --git a/.github/workflows/delete-blacksmith-cache.yml b/.github/workflows/delete-blacksmith-cache.yml index aad432deb0b9ad..26addc991fecc6 100644 --- a/.github/workflows/delete-blacksmith-cache.yml +++ b/.github/workflows/delete-blacksmith-cache.yml @@ -1,4 +1,4 @@ -name: Manually Delete Blacksmith Cache +name: Delete Blacksmith Cache on: workflow_dispatch: inputs: @@ -6,12 +6,28 @@ on: description: "Blacksmith Cache Key to Delete" required: true type: string + pull_request: + types: [closed] + jobs: manually-delete-blacksmith-cache: + if: github.event_name == 'workflow_dispatch' runs-on: blacksmith-2vcpu-ubuntu-2404 steps: - name: Checkout uses: actions/checkout@v4 - uses: useblacksmith/cache-delete@v1 with: - cache_key: ${{ inputs.cache_key }} + key: ${{ inputs.cache_key }} + + delete-cache-build-on-pr-close: + if: github.event_name == 'pull_request' && github.event.action == 'closed' + runs-on: blacksmith-2vcpu-ubuntu-2404 + env: + CACHE_NAME: prod-build + steps: + - name: Delete cache-build cache + uses: useblacksmith/cache-delete@v1 + with: + key: ${{ env.CACHE_NAME }}-${{ github.event.pull_request.head.ref }} + prefix: "true" From d3e5cfb311d07ec368d4c8b18ee4e849b2e95400 Mon Sep 17 00:00:00 2001 From: "cal-com-ci[bot]" <247290566+cal-com-ci[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 14:29:47 +0000 Subject: [PATCH 06/11] feat: update translations via @LingoDotDev (#26313) Co-authored-by: Lingo.dev --- apps/web/public/static/locales/ar/common.json | 1 + apps/web/public/static/locales/az/common.json | 1 + apps/web/public/static/locales/bg/common.json | 1 + apps/web/public/static/locales/bn/common.json | 1 + apps/web/public/static/locales/ca/common.json | 1 + apps/web/public/static/locales/cs/common.json | 1 + apps/web/public/static/locales/da/common.json | 1 + apps/web/public/static/locales/de/common.json | 1 + apps/web/public/static/locales/el/common.json | 1 + apps/web/public/static/locales/es-419/common.json | 1 + apps/web/public/static/locales/es/common.json | 1 + apps/web/public/static/locales/et/common.json | 1 + apps/web/public/static/locales/eu/common.json | 1 + apps/web/public/static/locales/fi/common.json | 1 + apps/web/public/static/locales/fr/common.json | 1 + apps/web/public/static/locales/he/common.json | 1 + apps/web/public/static/locales/hu/common.json | 1 + apps/web/public/static/locales/it/common.json | 1 + apps/web/public/static/locales/ja/common.json | 1 + apps/web/public/static/locales/km/common.json | 1 + apps/web/public/static/locales/ko/common.json | 1 + apps/web/public/static/locales/nl/common.json | 1 + apps/web/public/static/locales/no/common.json | 1 + apps/web/public/static/locales/pl/common.json | 1 + apps/web/public/static/locales/pt-BR/common.json | 1 + apps/web/public/static/locales/pt/common.json | 1 + apps/web/public/static/locales/ro/common.json | 1 + apps/web/public/static/locales/ru/common.json | 1 + apps/web/public/static/locales/sk-SK/common.json | 1 + apps/web/public/static/locales/sr/common.json | 1 + apps/web/public/static/locales/sv/common.json | 1 + apps/web/public/static/locales/tr/common.json | 1 + apps/web/public/static/locales/uk/common.json | 1 + apps/web/public/static/locales/vi/common.json | 1 + apps/web/public/static/locales/zh-CN/common.json | 1 + apps/web/public/static/locales/zh-TW/common.json | 1 + i18n.lock | 1 + 37 files changed, 37 insertions(+) diff --git a/apps/web/public/static/locales/ar/common.json b/apps/web/public/static/locales/ar/common.json index cf21eea11b5a7a..6689db8b6dd5ba 100644 --- a/apps/web/public/static/locales/ar/common.json +++ b/apps/web/public/static/locales/ar/common.json @@ -3493,6 +3493,7 @@ "last_active": "آخر نشاط", "member_since": "عضو منذ", "last_updated": "آخر تحديث", + "completed_onboarding": "اكتمل الإعداد", "salesforce_on_cancel_write_to_event": "عند إلغاء الحجز، اكتب في سجل الحدث بدلاً من حذف الحدث", "salesforce_on_every_cancellation": "عند كل إلغاء", "report_issue": "الإبلاغ عن مشكلة", diff --git a/apps/web/public/static/locales/az/common.json b/apps/web/public/static/locales/az/common.json index 581ff6b3a90c66..001afb8e2afbe2 100644 --- a/apps/web/public/static/locales/az/common.json +++ b/apps/web/public/static/locales/az/common.json @@ -3493,6 +3493,7 @@ "last_active": "Son aktivlik", "member_since": "Üzvlük tarixi", "last_updated": "Son yenilənmə", + "completed_onboarding": "Tanışlıq tamamlandı", "salesforce_on_cancel_write_to_event": "Rezervasiya ləğv edildikdə, tədbiri silmək əvəzinə tədbir qeydinə yazın", "salesforce_on_every_cancellation": "Hər ləğv zamanı", "report_issue": "Problemi bildirin", diff --git a/apps/web/public/static/locales/bg/common.json b/apps/web/public/static/locales/bg/common.json index ba87c0891a8916..dfa976315c4ecc 100644 --- a/apps/web/public/static/locales/bg/common.json +++ b/apps/web/public/static/locales/bg/common.json @@ -3493,6 +3493,7 @@ "last_active": "Последна активност", "member_since": "Член от", "last_updated": "Последно обновено", + "completed_onboarding": "Завършено въвеждане", "salesforce_on_cancel_write_to_event": "При отменена резервация, запиши в записа на събитието вместо да изтриеш събитието", "salesforce_on_every_cancellation": "При всяка отмяна", "report_issue": "Докладвай проблем", diff --git a/apps/web/public/static/locales/bn/common.json b/apps/web/public/static/locales/bn/common.json index 2c357da03bfd67..90ea99fbfbda9d 100644 --- a/apps/web/public/static/locales/bn/common.json +++ b/apps/web/public/static/locales/bn/common.json @@ -3493,6 +3493,7 @@ "last_active": "সর্বশেষ সক্রিয়", "member_since": "সদস্য হওয়ার তারিখ", "last_updated": "সর্বশেষ আপডেট", + "completed_onboarding": "অনবোর্ডিং সম্পন্ন হয়েছে", "salesforce_on_cancel_write_to_event": "বাতিল করা বুকিংয়ের ক্ষেত্রে, ইভেন্ট মুছে ফেলার পরিবর্তে ইভেন্ট রেকর্ডে লিখুন", "salesforce_on_every_cancellation": "প্রতিটি বাতিলকরণের সময়", "report_issue": "সমস্যা রিপোর্ট করুন", diff --git a/apps/web/public/static/locales/ca/common.json b/apps/web/public/static/locales/ca/common.json index de325f2bb62d91..5dfee2581cd540 100644 --- a/apps/web/public/static/locales/ca/common.json +++ b/apps/web/public/static/locales/ca/common.json @@ -3493,6 +3493,7 @@ "last_active": "Última activitat", "member_since": "Membre des de", "last_updated": "Última actualització", + "completed_onboarding": "Incorporació completada", "salesforce_on_cancel_write_to_event": "En cancel·lar la reserva, escriu al registre d'esdeveniments en lloc d'eliminar l'esdeveniment", "salesforce_on_every_cancellation": "En cada cancel·lació", "report_issue": "Informar d'un problema", diff --git a/apps/web/public/static/locales/cs/common.json b/apps/web/public/static/locales/cs/common.json index a33e9e255f2f1e..f5d6384ca4a752 100644 --- a/apps/web/public/static/locales/cs/common.json +++ b/apps/web/public/static/locales/cs/common.json @@ -3493,6 +3493,7 @@ "last_active": "Naposledy aktivní", "member_since": "Členem od", "last_updated": "Naposledy aktualizováno", + "completed_onboarding": "Dokončené úvodní nastavení", "salesforce_on_cancel_write_to_event": "Při zrušení rezervace zapsat do záznamu události místo smazání události", "salesforce_on_every_cancellation": "Při každém zrušení", "report_issue": "Nahlásit problém", diff --git a/apps/web/public/static/locales/da/common.json b/apps/web/public/static/locales/da/common.json index 2601d251db0ec2..ae90aeccce9edf 100644 --- a/apps/web/public/static/locales/da/common.json +++ b/apps/web/public/static/locales/da/common.json @@ -3493,6 +3493,7 @@ "last_active": "Sidst aktiv", "member_since": "Medlem siden", "last_updated": "Sidst opdateret", + "completed_onboarding": "Onboarding gennemført", "salesforce_on_cancel_write_to_event": "Ved aflyst booking, skriv til begivenhedsoptegnelsen i stedet for at slette begivenheden", "salesforce_on_every_cancellation": "Ved hver aflysning", "report_issue": "Rapportér problem", diff --git a/apps/web/public/static/locales/de/common.json b/apps/web/public/static/locales/de/common.json index d0cb2b75fb76e3..5091b98f96b388 100644 --- a/apps/web/public/static/locales/de/common.json +++ b/apps/web/public/static/locales/de/common.json @@ -3493,6 +3493,7 @@ "last_active": "Zuletzt aktiv", "member_since": "Mitglied seit", "last_updated": "Zuletzt aktualisiert", + "completed_onboarding": "Onboarding abgeschlossen", "salesforce_on_cancel_write_to_event": "Bei stornierter Buchung in den Ereignisdatensatz schreiben, anstatt das Ereignis zu löschen", "salesforce_on_every_cancellation": "Bei jeder Stornierung", "report_issue": "Problem melden", diff --git a/apps/web/public/static/locales/el/common.json b/apps/web/public/static/locales/el/common.json index abc3fcd1449a4e..b737fb63e8259d 100644 --- a/apps/web/public/static/locales/el/common.json +++ b/apps/web/public/static/locales/el/common.json @@ -3493,6 +3493,7 @@ "last_active": "Τελευταία ενεργός", "member_since": "Μέλος από", "last_updated": "Τελευταία ενημέρωση", + "completed_onboarding": "Ολοκληρωμένη εισαγωγή", "salesforce_on_cancel_write_to_event": "Σε ακύρωση κράτησης, εγγραφή στο αρχείο συμβάντος αντί για διαγραφή συμβάντος", "salesforce_on_every_cancellation": "Σε κάθε ακύρωση", "report_issue": "Αναφορά προβλήματος", diff --git a/apps/web/public/static/locales/es-419/common.json b/apps/web/public/static/locales/es-419/common.json index 07458444408040..d3d650f5b171ce 100644 --- a/apps/web/public/static/locales/es-419/common.json +++ b/apps/web/public/static/locales/es-419/common.json @@ -3493,6 +3493,7 @@ "last_active": "Última actividad", "member_since": "Miembro desde", "last_updated": "Última actualización", + "completed_onboarding": "Configuración inicial completada", "salesforce_on_cancel_write_to_event": "Al cancelar una reserva, escribir en el registro del evento en lugar de eliminarlo", "salesforce_on_every_cancellation": "En cada cancelación", "report_issue": "Reportar problema", diff --git a/apps/web/public/static/locales/es/common.json b/apps/web/public/static/locales/es/common.json index 134e214209bc4e..78a0a63b8e746b 100644 --- a/apps/web/public/static/locales/es/common.json +++ b/apps/web/public/static/locales/es/common.json @@ -3493,6 +3493,7 @@ "last_active": "Última actividad", "member_since": "Miembro desde", "last_updated": "Última actualización", + "completed_onboarding": "Configuración inicial completada", "salesforce_on_cancel_write_to_event": "Al cancelar la reserva, escribir en el registro del evento en lugar de eliminarlo", "salesforce_on_every_cancellation": "En cada cancelación", "report_issue": "Informar de un problema", diff --git a/apps/web/public/static/locales/et/common.json b/apps/web/public/static/locales/et/common.json index c552b3f38e78f8..c98fddb8dcfff4 100644 --- a/apps/web/public/static/locales/et/common.json +++ b/apps/web/public/static/locales/et/common.json @@ -3493,6 +3493,7 @@ "last_active": "Viimati aktiivne", "member_since": "Liige alates", "last_updated": "Viimati uuendatud", + "completed_onboarding": "Sisseelamine lõpetatud", "salesforce_on_cancel_write_to_event": "Tühistatud broneeringu korral kirjutage sündmuse kirjesse selle asemel, et sündmus kustutada", "salesforce_on_every_cancellation": "Iga tühistamise korral", "report_issue": "Teata probleemist", diff --git a/apps/web/public/static/locales/eu/common.json b/apps/web/public/static/locales/eu/common.json index 2c5ca3783b5685..f0549773616584 100644 --- a/apps/web/public/static/locales/eu/common.json +++ b/apps/web/public/static/locales/eu/common.json @@ -3493,6 +3493,7 @@ "last_active": "Azken aldiz aktibo", "member_since": "Kide noiztik", "last_updated": "Azken eguneratzea", + "completed_onboarding": "Hasierako prozesua amaituta", "salesforce_on_cancel_write_to_event": "Erreserba bertan behera uztean, idatzi gertaera erregistroan gertaera ezabatu beharrean", "salesforce_on_every_cancellation": "Bertan behera uzte bakoitzean", "report_issue": "Arazoa jakinarazi", diff --git a/apps/web/public/static/locales/fi/common.json b/apps/web/public/static/locales/fi/common.json index 59bc6d8147273c..39d27d3f79c4d1 100644 --- a/apps/web/public/static/locales/fi/common.json +++ b/apps/web/public/static/locales/fi/common.json @@ -3493,6 +3493,7 @@ "last_active": "Viimeksi aktiivinen", "member_since": "Jäsen alkaen", "last_updated": "Viimeksi päivitetty", + "completed_onboarding": "Käyttöönotto suoritettu", "salesforce_on_cancel_write_to_event": "Peruutetun varauksen yhteydessä kirjoita tapahtuman tietueeseen tapahtuman poistamisen sijaan", "salesforce_on_every_cancellation": "Jokaisessa peruutuksessa", "report_issue": "Ilmoita ongelmasta", diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index 895b895aa4e338..c836e702288093 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -3493,6 +3493,7 @@ "last_active": "Dernière activité", "member_since": "Membre depuis", "last_updated": "Dernière mise à jour", + "completed_onboarding": "Intégration terminée", "salesforce_on_cancel_write_to_event": "Lors de l'annulation d'une réservation, écrire dans l'enregistrement de l'événement au lieu de le supprimer", "salesforce_on_every_cancellation": "À chaque annulation", "report_issue": "Signaler un problème", diff --git a/apps/web/public/static/locales/he/common.json b/apps/web/public/static/locales/he/common.json index a280f3b876ddc4..734a8a9aa2be22 100644 --- a/apps/web/public/static/locales/he/common.json +++ b/apps/web/public/static/locales/he/common.json @@ -3493,6 +3493,7 @@ "last_active": "פעיל לאחרונה", "member_since": "חבר מאז", "last_updated": "עודכן לאחרונה", + "completed_onboarding": "תהליך ההכנה הושלם", "salesforce_on_cancel_write_to_event": "בעת ביטול הזמנה, כתוב לרשומת האירוע במקום למחוק את האירוע", "salesforce_on_every_cancellation": "בכל ביטול", "report_issue": "דווח על בעיה", diff --git a/apps/web/public/static/locales/hu/common.json b/apps/web/public/static/locales/hu/common.json index ff6780f410cb10..80e4d1d8fbdd1d 100644 --- a/apps/web/public/static/locales/hu/common.json +++ b/apps/web/public/static/locales/hu/common.json @@ -3493,6 +3493,7 @@ "last_active": "Utoljára aktív", "member_since": "Tag mióta", "last_updated": "Utoljára frissítve", + "completed_onboarding": "Beállítás befejezve", "salesforce_on_cancel_write_to_event": "Lemondott foglalás esetén írjon az esemény rekordba az esemény törlése helyett", "salesforce_on_every_cancellation": "Minden lemondás esetén", "report_issue": "Probléma jelentése", diff --git a/apps/web/public/static/locales/it/common.json b/apps/web/public/static/locales/it/common.json index c9db26220955de..f319a50ab4f7a1 100644 --- a/apps/web/public/static/locales/it/common.json +++ b/apps/web/public/static/locales/it/common.json @@ -3493,6 +3493,7 @@ "last_active": "Ultima attività", "member_since": "Membro dal", "last_updated": "Ultimo aggiornamento", + "completed_onboarding": "Onboarding completato", "salesforce_on_cancel_write_to_event": "In caso di prenotazione annullata, scrivi nel record dell'evento invece di eliminarlo", "salesforce_on_every_cancellation": "Ad ogni cancellazione", "report_issue": "Segnala un problema", diff --git a/apps/web/public/static/locales/ja/common.json b/apps/web/public/static/locales/ja/common.json index aa0fd6be219bca..dc762198451d04 100644 --- a/apps/web/public/static/locales/ja/common.json +++ b/apps/web/public/static/locales/ja/common.json @@ -3493,6 +3493,7 @@ "last_active": "最終アクティブ", "member_since": "登録日", "last_updated": "最終更新日", + "completed_onboarding": "オンボーディング完了", "salesforce_on_cancel_write_to_event": "予約キャンセル時、イベントを削除せずにイベントレコードに書き込む", "salesforce_on_every_cancellation": "すべてのキャンセル時", "report_issue": "問題を報告", diff --git a/apps/web/public/static/locales/km/common.json b/apps/web/public/static/locales/km/common.json index 352eae15aebaec..27cad0f9b03529 100644 --- a/apps/web/public/static/locales/km/common.json +++ b/apps/web/public/static/locales/km/common.json @@ -3493,6 +3493,7 @@ "last_active": "សកម្មភាពចុងក្រោយ", "member_since": "ជាសមាជិកតាំងពី", "last_updated": "បានធ្វើបច្ចុប្បន្នភាពចុងក្រោយ", + "completed_onboarding": "បានបញ្ចប់ការណែនាំ", "salesforce_on_cancel_write_to_event": "នៅពេលលុបចោលការកក់ សូមសរសេរទៅកាន់កំណត់ត្រាព្រឹត្តិការណ៍ជំនួសឱ្យការលុបព្រឹត្តិការណ៍", "salesforce_on_every_cancellation": "នៅរាល់ពេលលុបចោល", "report_issue": "រាយការណ៍បញ្ហា", diff --git a/apps/web/public/static/locales/ko/common.json b/apps/web/public/static/locales/ko/common.json index dbc97a058bd081..4d8b1b154890d8 100644 --- a/apps/web/public/static/locales/ko/common.json +++ b/apps/web/public/static/locales/ko/common.json @@ -3493,6 +3493,7 @@ "last_active": "마지막 활동", "member_since": "가입일", "last_updated": "마지막 업데이트", + "completed_onboarding": "온보딩 완료됨", "salesforce_on_cancel_write_to_event": "예약 취소 시 이벤트 삭제 대신 이벤트 기록에 작성", "salesforce_on_every_cancellation": "모든 취소 시", "report_issue": "문제 신고", diff --git a/apps/web/public/static/locales/nl/common.json b/apps/web/public/static/locales/nl/common.json index e1f803f2266dd3..cf4c51ceb510b1 100644 --- a/apps/web/public/static/locales/nl/common.json +++ b/apps/web/public/static/locales/nl/common.json @@ -3493,6 +3493,7 @@ "last_active": "Laatst actief", "member_since": "Lid sinds", "last_updated": "Laatst bijgewerkt", + "completed_onboarding": "Onboarding voltooid", "salesforce_on_cancel_write_to_event": "Bij geannuleerde boeking, schrijf naar evenementrecord in plaats van evenement verwijderen", "salesforce_on_every_cancellation": "Bij elke annulering", "report_issue": "Probleem melden", diff --git a/apps/web/public/static/locales/no/common.json b/apps/web/public/static/locales/no/common.json index d77f55a8733996..4cbbda06c98e60 100644 --- a/apps/web/public/static/locales/no/common.json +++ b/apps/web/public/static/locales/no/common.json @@ -3493,6 +3493,7 @@ "last_active": "Sist aktiv", "member_since": "Medlem siden", "last_updated": "Sist oppdatert", + "completed_onboarding": "Fullført onboarding", "salesforce_on_cancel_write_to_event": "Ved kansellert booking, skriv til hendelsesposten i stedet for å slette hendelsen", "salesforce_on_every_cancellation": "Ved hver kansellering", "report_issue": "Rapporter problem", diff --git a/apps/web/public/static/locales/pl/common.json b/apps/web/public/static/locales/pl/common.json index 2a66b6bcd4bae5..d23094810abb9a 100644 --- a/apps/web/public/static/locales/pl/common.json +++ b/apps/web/public/static/locales/pl/common.json @@ -3493,6 +3493,7 @@ "last_active": "Ostatnia aktywność", "member_since": "Członek od", "last_updated": "Ostatnia aktualizacja", + "completed_onboarding": "Wdrażanie zakończone", "salesforce_on_cancel_write_to_event": "W przypadku odwołanej rezerwacji zapisz w rekordzie wydarzenia zamiast usuwać wydarzenie", "salesforce_on_every_cancellation": "Przy każdej anulacji", "report_issue": "Zgłoś problem", diff --git a/apps/web/public/static/locales/pt-BR/common.json b/apps/web/public/static/locales/pt-BR/common.json index 690318231c1301..3d6f64408d75a2 100644 --- a/apps/web/public/static/locales/pt-BR/common.json +++ b/apps/web/public/static/locales/pt-BR/common.json @@ -3493,6 +3493,7 @@ "last_active": "Último ativo", "member_since": "Membro desde", "last_updated": "Última atualização", + "completed_onboarding": "Integração concluída", "salesforce_on_cancel_write_to_event": "Ao cancelar a reserva, escrever no registro do evento em vez de excluí-lo", "salesforce_on_every_cancellation": "Em cada cancelamento", "report_issue": "Reportar problema", diff --git a/apps/web/public/static/locales/pt/common.json b/apps/web/public/static/locales/pt/common.json index bd5f4e49657b2a..961a9d07cf1503 100644 --- a/apps/web/public/static/locales/pt/common.json +++ b/apps/web/public/static/locales/pt/common.json @@ -3493,6 +3493,7 @@ "last_active": "Última atividade", "member_since": "Membro desde", "last_updated": "Última atualização", + "completed_onboarding": "Integração concluída", "salesforce_on_cancel_write_to_event": "Ao cancelar a reserva, escrever no registro do evento em vez de excluí-lo", "salesforce_on_every_cancellation": "Em cada cancelamento", "report_issue": "Reportar problema", diff --git a/apps/web/public/static/locales/ro/common.json b/apps/web/public/static/locales/ro/common.json index da1c2250da3f04..d04fa56c70ce3d 100644 --- a/apps/web/public/static/locales/ro/common.json +++ b/apps/web/public/static/locales/ro/common.json @@ -3493,6 +3493,7 @@ "last_active": "Ultima activitate", "member_since": "Membru din", "last_updated": "Ultima actualizare", + "completed_onboarding": "Onboarding finalizat", "salesforce_on_cancel_write_to_event": "La anularea rezervării, scrie în înregistrarea evenimentului în loc să ștergi evenimentul", "salesforce_on_every_cancellation": "La fiecare anulare", "report_issue": "Raportează o problemă", diff --git a/apps/web/public/static/locales/ru/common.json b/apps/web/public/static/locales/ru/common.json index b339556bad4f14..435703b615bc6a 100644 --- a/apps/web/public/static/locales/ru/common.json +++ b/apps/web/public/static/locales/ru/common.json @@ -3493,6 +3493,7 @@ "last_active": "Последняя активность", "member_since": "Участник с", "last_updated": "Последнее обновление", + "completed_onboarding": "Адаптация завершена", "salesforce_on_cancel_write_to_event": "При отмене бронирования записывать в запись события вместо удаления события", "salesforce_on_every_cancellation": "При каждой отмене", "report_issue": "Сообщить о проблеме", diff --git a/apps/web/public/static/locales/sk-SK/common.json b/apps/web/public/static/locales/sk-SK/common.json index c07373c3af7878..0a986f842cf7c8 100644 --- a/apps/web/public/static/locales/sk-SK/common.json +++ b/apps/web/public/static/locales/sk-SK/common.json @@ -3493,6 +3493,7 @@ "last_active": "Naposledy aktívny", "member_since": "Členom od", "last_updated": "Naposledy aktualizované", + "completed_onboarding": "Dokončené úvodné nastavenie", "salesforce_on_cancel_write_to_event": "Pri zrušenej rezervácii zapísať do záznamu udalosti namiesto vymazania udalosti", "salesforce_on_every_cancellation": "Pri každom zrušení", "report_issue": "Nahlásiť problém", diff --git a/apps/web/public/static/locales/sr/common.json b/apps/web/public/static/locales/sr/common.json index c5c100f0d80a68..db54bb4d601706 100644 --- a/apps/web/public/static/locales/sr/common.json +++ b/apps/web/public/static/locales/sr/common.json @@ -3493,6 +3493,7 @@ "last_active": "Poslednja aktivnost", "member_since": "Član od", "last_updated": "Poslednje ažuriranje", + "completed_onboarding": "Završeno uvođenje", "salesforce_on_cancel_write_to_event": "Pri otkazivanju rezervacije, upiši u evidenciju događaja umesto brisanja događaja", "salesforce_on_every_cancellation": "Pri svakom otkazivanju", "report_issue": "Prijavi problem", diff --git a/apps/web/public/static/locales/sv/common.json b/apps/web/public/static/locales/sv/common.json index 099597bb5e626b..05593d26bc0966 100644 --- a/apps/web/public/static/locales/sv/common.json +++ b/apps/web/public/static/locales/sv/common.json @@ -3493,6 +3493,7 @@ "last_active": "Senast aktiv", "member_since": "Medlem sedan", "last_updated": "Senast uppdaterad", + "completed_onboarding": "Onboarding slutförd", "salesforce_on_cancel_write_to_event": "Vid avbokning, skriv till händelseregistret istället för att ta bort händelsen", "salesforce_on_every_cancellation": "Vid varje avbokning", "report_issue": "Rapportera problem", diff --git a/apps/web/public/static/locales/tr/common.json b/apps/web/public/static/locales/tr/common.json index bbd961331c2c5c..b167ba03eea1f2 100644 --- a/apps/web/public/static/locales/tr/common.json +++ b/apps/web/public/static/locales/tr/common.json @@ -3493,6 +3493,7 @@ "last_active": "Son Aktif", "member_since": "Üyelik Başlangıcı", "last_updated": "Son Güncelleme", + "completed_onboarding": "Oryantasyon tamamlandı", "salesforce_on_cancel_write_to_event": "İptal edilen rezervasyonda, etkinliği silmek yerine etkinlik kaydına yaz", "salesforce_on_every_cancellation": "Her iptal işleminde", "report_issue": "Sorun bildir", diff --git a/apps/web/public/static/locales/uk/common.json b/apps/web/public/static/locales/uk/common.json index 0abebf8c646c3a..502bede445b188 100644 --- a/apps/web/public/static/locales/uk/common.json +++ b/apps/web/public/static/locales/uk/common.json @@ -3493,6 +3493,7 @@ "last_active": "Остання активність", "member_since": "Учасник з", "last_updated": "Останнє оновлення", + "completed_onboarding": "Онбординг завершено", "salesforce_on_cancel_write_to_event": "При скасуванні бронювання, записувати в запис події замість видалення події", "salesforce_on_every_cancellation": "При кожному скасуванні", "report_issue": "Повідомити про проблему", diff --git a/apps/web/public/static/locales/vi/common.json b/apps/web/public/static/locales/vi/common.json index 4d01d58ec10718..820a46832011c5 100644 --- a/apps/web/public/static/locales/vi/common.json +++ b/apps/web/public/static/locales/vi/common.json @@ -3493,6 +3493,7 @@ "last_active": "Hoạt động lần cuối", "member_since": "Thành viên từ", "last_updated": "Cập nhật lần cuối", + "completed_onboarding": "Đã hoàn thành thiết lập", "salesforce_on_cancel_write_to_event": "Khi hủy đặt lịch, ghi vào bản ghi sự kiện thay vì xóa sự kiện", "salesforce_on_every_cancellation": "Với mỗi lần hủy", "report_issue": "Báo cáo vấn đề", diff --git a/apps/web/public/static/locales/zh-CN/common.json b/apps/web/public/static/locales/zh-CN/common.json index d6b5692981f23c..6185505c0a4f56 100644 --- a/apps/web/public/static/locales/zh-CN/common.json +++ b/apps/web/public/static/locales/zh-CN/common.json @@ -3493,6 +3493,7 @@ "last_active": "上次活跃", "member_since": "会员自", "last_updated": "最后更新", + "completed_onboarding": "引导已完成", "salesforce_on_cancel_write_to_event": "在取消预订时,写入事件记录而不是删除事件", "salesforce_on_every_cancellation": "每次取消时", "report_issue": "报告问题", diff --git a/apps/web/public/static/locales/zh-TW/common.json b/apps/web/public/static/locales/zh-TW/common.json index 5db09d83bdd1cb..c9f41537875122 100644 --- a/apps/web/public/static/locales/zh-TW/common.json +++ b/apps/web/public/static/locales/zh-TW/common.json @@ -3493,6 +3493,7 @@ "last_active": "最近活動", "member_since": "成員自", "last_updated": "最後更新", + "completed_onboarding": "已完成入門設置", "salesforce_on_cancel_write_to_event": "在取消預約時,寫入事件記錄而不是刪除事件", "salesforce_on_every_cancellation": "每次取消時", "report_issue": "回報問題", diff --git a/i18n.lock b/i18n.lock index f943900380cebf..0e8a3582a82715 100644 --- a/i18n.lock +++ b/i18n.lock @@ -3490,6 +3490,7 @@ checksums: last_active: f56f696ea3c36a1084fe7fab1c422b74 member_since: 68f13de9a1bc207db0d8b62c6b433052 last_updated: 956353f0af47fe60f0f45ec25d129a01 + completed_onboarding: 264710464ed3a726be025bdbff860db5 salesforce_on_cancel_write_to_event: 5d82dd4959cc2ae009a750f096ad74e0 salesforce_on_every_cancellation: a54453f804195b3747564b1ceb0d60b4 report_issue: 5abe40470cb5476d6e4f49fe422bd438 From 3410ae6795d051d66c85a0a979a4b0beeb646072 Mon Sep 17 00:00:00 2001 From: "cal-com-ci[bot]" <247290566+cal-com-ci[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 15:28:55 +0000 Subject: [PATCH 07/11] feat: update translations via @LingoDotDev (#26315) Co-authored-by: Lingo.dev From 1dbbc934ccdc1fe2defc3c52d95a80eb40268569 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Tue, 30 Dec 2025 12:29:33 -0300 Subject: [PATCH 08/11] chore: use lookup-only cache check to skip deps job downloads (#26314) * fix(ci): skip yarn install in deps job when cache is hit The Install Dependencies / Yarn install & cache step in pr.yml is primarily for populating the cache when nothing is cached. When cache keys are found, we can skip the actual yarn install and let the workflow carry on quickly. This adds a skip-install-if-cache-hit parameter to the yarn-install action that allows skipping the install when node_modules cache is hit. This is enabled for the deps job in yarn-install.yml but not for other jobs that actually need node_modules for their work. Co-Authored-By: keith@cal.com * fix(ci): also skip playwright install in deps job when cache is hit Extends the skip-install-if-cache-hit parameter to the yarn-playwright-install action as well, so both yarn install and playwright install are skipped in the deps job when their respective caches are hit. Co-Authored-By: keith@cal.com * fix(ci): use lookup-only cache check to avoid downloading 1.2GB When skip-install-if-cache-hit is true, use actions/cache/restore@v4 with lookup-only: true to check if caches exist without downloading them. This avoids downloading ~1.2GB of cache data when we just want to verify caches exist in the deps job. The flow is now: 1. If skip-install-if-cache-hit is true, run lookup-only checks for all caches 2. If all caches hit, skip the entire restore + install flow (no downloads) 3. If any cache misses, fall back to normal restore + install + save behavior This optimization only applies when skip-install-if-cache-hit is set to true, so other jobs that need node_modules continue to work normally. Co-Authored-By: keith@cal.com * Apply suggestion from @keithwillcode * Apply suggestion from @keithwillcode * Apply suggestion from @keithwillcode * Apply suggestion from @keithwillcode * fix(ci): address @cubic-dev-ai feedback on cache conditions 1. yarn-install: Add 'all-caches-check' step to compute whether all three caches hit (not just node_modules). This ensures we only bail out when everything is cached, matching the PR description. 2. yarn-playwright-install: Fix backward compatibility for install step. In default mode, check playwright-cache.outputs.cache-hit (the restore step). In skip mode, check playwright-cache-check.outputs.cache-hit (lookup-only). Co-Authored-By: keith@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/actions/yarn-install/action.yml | 54 +++++++++++++++++++ .../yarn-playwright-install/action.yml | 26 ++++++++- .github/workflows/yarn-install.yml | 4 ++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/.github/actions/yarn-install/action.yml b/.github/actions/yarn-install/action.yml index c48a484f0f0a87..a930ce326d37ef 100644 --- a/.github/actions/yarn-install/action.yml +++ b/.github/actions/yarn-install/action.yml @@ -15,6 +15,10 @@ inputs: node_version: required: false default: v20.x + skip-install-if-cache-hit: + description: "Skip yarn install if node_modules cache is hit. Use this for jobs that only need to check that cache exists." + required: false + default: "false" runs: using: "composite" @@ -29,9 +33,56 @@ runs: run: | echo "CACHE_FOLDER=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT + # When skip-install-if-cache-hit is true, first check if all caches exist without downloading (lookup-only mode) + # This avoids downloading ~1.2GB of cache data when we just want to verify caches exist + - name: Check yarn cache (lookup-only) + if: ${{ inputs.skip-install-if-cache-hit == 'true' }} + uses: actions/cache/restore@v4 + id: yarn-download-cache-check + with: + path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }} + key: yarn-download-cache-${{ hashFiles('yarn.lock') }} + lookup-only: true + + - name: Check node_modules cache (lookup-only) + if: ${{ inputs.skip-install-if-cache-hit == 'true' }} + uses: actions/cache/restore@v4 + id: yarn-nm-cache-check + with: + path: | + **/node_modules/ + !**/.next/node_modules/ + key: ${{ runner.os }}-yarn-nm-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }} + lookup-only: true + + - name: Check yarn install state cache (lookup-only) + if: ${{ inputs.skip-install-if-cache-hit == 'true' }} + uses: actions/cache/restore@v4 + id: yarn-install-state-cache-check + with: + path: .yarn/ci-cache/ + key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }} + lookup-only: true + + # Compute whether all caches hit (only in skip mode) + - name: Check if all caches hit + if: ${{ inputs.skip-install-if-cache-hit == 'true' }} + id: all-caches-check + shell: bash + run: | + if [[ "${{ steps.yarn-download-cache-check.outputs.cache-hit }}" == "true" && \ + "${{ steps.yarn-nm-cache-check.outputs.cache-hit }}" == "true" && \ + "${{ steps.yarn-install-state-cache-check.outputs.cache-hit }}" == "true" ]]; then + echo "all-hit=true" >> $GITHUB_OUTPUT + else + echo "all-hit=false" >> $GITHUB_OUTPUT + fi + # Yarn rotates the downloaded cache archives, @see https://github.com/actions/setup-node/issues/325 # Yarn cache is also reusable between arch and os. + # Only restore if not in skip mode, or if skip mode but any cache check missed - name: Restore yarn cache + if: ${{ inputs.skip-install-if-cache-hit != 'true' || steps.all-caches-check.outputs.all-hit != 'true' }} uses: actions/cache@v4 id: yarn-download-cache with: @@ -40,6 +91,7 @@ runs: # Invalidated on yarn.lock changes - name: Restore node_modules + if: ${{ inputs.skip-install-if-cache-hit != 'true' || steps.all-caches-check.outputs.all-hit != 'true' }} id: yarn-nm-cache uses: actions/cache@v4 with: @@ -50,6 +102,7 @@ runs: # Invalidated on yarn.lock changes - name: Restore yarn install state + if: ${{ inputs.skip-install-if-cache-hit != 'true' || steps.all-caches-check.outputs.all-hit != 'true' }} id: yarn-install-state-cache uses: actions/cache@v4 with: @@ -57,6 +110,7 @@ runs: key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }} - name: Install dependencies + if: ${{ inputs.skip-install-if-cache-hit != 'true' || steps.all-caches-check.outputs.all-hit != 'true' }} shell: bash run: | yarn install --inline-builds diff --git a/.github/actions/yarn-playwright-install/action.yml b/.github/actions/yarn-playwright-install/action.yml index 7d444283c1f210..ea3ae0469b8d0d 100644 --- a/.github/actions/yarn-playwright-install/action.yml +++ b/.github/actions/yarn-playwright-install/action.yml @@ -1,5 +1,10 @@ name: Install playwright binaries description: "Install playwright, cache and restore if necessary" +inputs: + skip-install-if-cache-hit: + description: "Skip playwright install if cache is hit. Use this for jobs that only need to check that cache exists." + required: false + default: "false" runs: using: "composite" steps: @@ -7,7 +12,23 @@ runs: shell: bash id: playwright-version run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package.json').devDependencies['@playwright/test'])")" >> $GITHUB_ENV + + # When skip-install-if-cache-hit is true, first check if cache exists without downloading (lookup-only mode) + - name: Check playwright cache (lookup-only) + if: ${{ inputs.skip-install-if-cache-hit == 'true' }} + id: playwright-cache-check + uses: actions/cache/restore@v4 + with: + path: | + ~/Library/Caches/ms-playwright + ~/.cache/ms-playwright + ${{ github.workspace }}/node_modules/playwright + key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} + lookup-only: true + + # Only restore cache if not in skip mode, or if skip mode but cache check missed - name: Cache playwright binaries + if: ${{ inputs.skip-install-if-cache-hit != 'true' || steps.playwright-cache-check.outputs.cache-hit != 'true' }} id: playwright-cache uses: actions/cache@v4 with: @@ -16,7 +37,10 @@ runs: ~/.cache/ms-playwright ${{ github.workspace }}/node_modules/playwright key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} + + # In default mode: run if playwright-cache missed + # In skip mode: run if playwright-cache-check missed (but cache restore step was also skipped) - name: Yarn playwright install - if: steps.playwright-cache.outputs.cache-hit != 'true' + if: ${{ (inputs.skip-install-if-cache-hit != 'true' && steps.playwright-cache.outputs.cache-hit != 'true') || (inputs.skip-install-if-cache-hit == 'true' && steps.playwright-cache-check.outputs.cache-hit != 'true') }} shell: bash run: yarn playwright install --with-deps diff --git a/.github/workflows/yarn-install.yml b/.github/workflows/yarn-install.yml index 81eb75d63048e9..e9e4c6ca5935eb 100644 --- a/.github/workflows/yarn-install.yml +++ b/.github/workflows/yarn-install.yml @@ -15,4 +15,8 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install + with: + skip-install-if-cache-hit: "true" - uses: ./.github/actions/yarn-playwright-install + with: + skip-install-if-cache-hit: "true" From 319d9858d46991e36d080d3391360fa6726bb90d Mon Sep 17 00:00:00 2001 From: Ram Shukla Date: Tue, 30 Dec 2025 21:07:46 +0530 Subject: [PATCH 09/11] fix: improve Insights empty state responsive design at 320px (#26203) * fix: improve Insights empty state responsive design at 320px * Update apps/web/modules/shell/UpgradeTip.tsx Co-authored-by: Keith Williams --------- Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Co-authored-by: Keith Williams --- apps/web/modules/shell/UpgradeTip.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/web/modules/shell/UpgradeTip.tsx b/apps/web/modules/shell/UpgradeTip.tsx index 87a34353f9753f..8545182bdc51e9 100644 --- a/apps/web/modules/shell/UpgradeTip.tsx +++ b/apps/web/modules/shell/UpgradeTip.tsx @@ -45,18 +45,18 @@ export function UpgradeTip({ return ( <>
- - + + {" "} + {title} -
-

{title}

-

{description}

+
+

{title}

+

{description}

{buttons}
From bc6ff386a4e75c8adc7121228c731bbc82ac1764 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Tue, 30 Dec 2025 13:03:10 -0300 Subject: [PATCH 10/11] refactor: move eventTypeSlug and eventTypeLocations to @calcom/lib/zod (#26115) * refactor: move eventTypeSlug and eventTypeLocations to @calcom/lib/zod Move shared Zod schemas from @calcom/prisma/zod-utils to @calcom/lib/zod to avoid prisma imports in non-repository code. Changes: - Create packages/lib/zod/eventType.ts with eventTypeSlug, eventTypeLocations, and EventTypeLocation type - Re-export from @calcom/prisma/zod-utils for backwards compatibility - Update packages/features/eventtypes/lib/schemas.ts to import from @calcom/lib/zod - Remove unused slugify function from zod-utils.ts This allows files like schemas.ts to avoid importing from @calcom/prisma, which helps maintain the architectural boundary that prisma should only be imported in repository code. Co-Authored-By: keith@cal.com * refactor: remove barrel file and use explicit imports Address PR feedback: - Delete packages/lib/zod/index.ts barrel file - Update imports to use explicit path @calcom/lib/zod/eventType - Simplify EventTypeLocation type to use z.infer Co-Authored-By: keith@cal.com * refactor: use explicit type definition with z.ZodType for eventTypeLocations Co-Authored-By: keith@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/features/eventtypes/lib/schemas.ts | 3 +- packages/lib/zod/eventType.ts | 49 ++++++++++++++++ packages/prisma/zod-utils.ts | 65 ++------------------- 3 files changed, 55 insertions(+), 62 deletions(-) create mode 100644 packages/lib/zod/eventType.ts diff --git a/packages/features/eventtypes/lib/schemas.ts b/packages/features/eventtypes/lib/schemas.ts index 30b467797d5fa2..6aa18a8d59ac26 100644 --- a/packages/features/eventtypes/lib/schemas.ts +++ b/packages/features/eventtypes/lib/schemas.ts @@ -1,7 +1,8 @@ import { z } from "zod"; +import { eventTypeLocations, eventTypeSlug } from "@calcom/lib/zod/eventType"; import { SchedulingType } from "@calcom/prisma/enums"; -import { eventTypeLocations, EventTypeMetaDataSchema, eventTypeSlug } from "@calcom/prisma/zod-utils"; +import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; type CalVideoSettings = | { diff --git a/packages/lib/zod/eventType.ts b/packages/lib/zod/eventType.ts new file mode 100644 index 00000000000000..5d6c134c256276 --- /dev/null +++ b/packages/lib/zod/eventType.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; + +import slugify from "../slugify"; + +/** + * Type definition for event type location + * Moved from @calcom/prisma/zod-utils to avoid prisma imports in non-repository code + */ +export type EventTypeLocation = { + type: string; + address?: string; + link?: string; + displayLocationPublicly?: boolean; + hostPhoneNumber?: string; + credentialId?: number; + teamName?: string; + customLabel?: string; +}; + +/** + * Schema for event type locations + * Validates an array of location objects for event types + */ +export const eventTypeLocations: z.ZodType = z.array( + z.object({ + // TODO: Couldn't find a way to make it a union of types from App Store locations + // Creating a dynamic union by iterating over the object doesn't seem to make TS happy + type: z.string(), + address: z.string().optional(), + link: z.string().url().optional(), + displayLocationPublicly: z.boolean().optional(), + hostPhoneNumber: z.string().optional(), + credentialId: z.number().optional(), + teamName: z.string().optional(), + customLabel: z.string().optional(), + }) +); + +/** + * Schema for event type slug + * Transforms and validates slugs for event types + */ +export const eventTypeSlug = z + .string() + .trim() + .transform((val) => slugify(val)) + .refine((val) => val.length >= 1, { + message: "Please enter at least one character", + }); diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index d245a4304a865f..9dd03b80d581d3 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -32,35 +32,6 @@ const emailRegexSchema = z .max(MAX_EMAIL_LENGTH, { message: "Email address is too long" }) .regex(emailRegex); -const slugify = (str: string, forDisplayingInput?: boolean) => { - if (!str) { - return ""; - } - - const s = str - .toLowerCase() // Convert to lowercase - .trim() // Remove whitespace from both sides - .normalize("NFD") // Normalize to decomposed form for handling accents - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - .replace(/\p{Diacritic}/gu, "") // Remove any diacritics (accents) from characters - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - .replace(/[^.\p{L}\p{N}\p{Zs}\p{Emoji}]+/gu, "-") // Replace any non-alphanumeric characters (including Unicode and except "." period) with a dash - .replace(/[\s_#]+/g, "-") // Replace whitespace, # and underscores with a single dash - .replace(/^-+/, "") // Remove dashes from start - .replace(/\.{2,}/g, ".") // Replace consecutive periods with a single period - .replace(/^\.+/, "") // Remove periods from the start - .replace( - /([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, - "" - ) // Removes emojis - .replace(/\s+/g, " ") - .replace(/-+/g, "-"); // Replace consecutive dashes with a single dash - - return forDisplayingInput ? s : s.replace(/-+$/, "").replace(/\.*$/, ""); // Remove dashes and period from end -}; - const getValidRhfFieldName = (fieldName: string) => { // Remember that any transformation that you do here would run on System Field names as well. So, be careful and avoiding doing anything here that would modify the SystemField names. // e.g. SystemField name currently have uppercases in them. So, no need to lowercase unless absolutely needed. @@ -326,31 +297,8 @@ export const bookingResponses = z export type BookingResponses = z.infer; -export type EventTypeLocation = { - type: string; - address?: string; - link?: string; - displayLocationPublicly?: boolean; - hostPhoneNumber?: string; - credentialId?: number; - teamName?: string; - customLabel?: string; -}; - -export const eventTypeLocations: z.ZodType = z.array( - z.object({ - // TODO: Couldn't find a way to make it a union of types from App Store locations - // Creating a dynamic union by iterating over the object doesn't seem to make TS happy - type: z.string(), - address: z.string().optional(), - link: z.string().url().optional(), - displayLocationPublicly: z.boolean().optional(), - hostPhoneNumber: z.string().optional(), - credentialId: z.number().optional(), - teamName: z.string().optional(), - customLabel: z.string().optional(), - }) -); +// Re-exported from @calcom/lib/zod/eventType for backwards compatibility +export { eventTypeLocations, type EventTypeLocation } from "@calcom/lib/zod/eventType"; // Matching RRule.Options: rrule/dist/esm/src/types.d.ts export const recurringEventType = z @@ -387,13 +335,8 @@ export const eventTypeColor = z export type IntervalLimitsType = IntervalLimit | null; -export const eventTypeSlug = z - .string() - .trim() - .transform((val) => slugify(val)) - .refine((val) => val.length >= 1, { - message: "Please enter at least one character", - }); +// Re-exported from @calcom/lib/zod/eventType for backwards compatibility +export { eventTypeSlug } from "@calcom/lib/zod/eventType"; export const stringToDate = z.string().transform((a) => new Date(a)); From 77ff8607d1482e0ab44c461ca593ef27291d5be8 Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Tue, 30 Dec 2025 22:50:29 +0530 Subject: [PATCH 11/11] test: flaky unit test (#26310) * fix: wip * test * Change pull_request to pull_request_target in workflow * test * update * fix unit test flakes * Update cache path to exclude node_modules Remove node_modules from cache path in action.yml --- .../intentToCreateOrg.handler.test.ts | 199 +++++++++++++----- 1 file changed, 144 insertions(+), 55 deletions(-) diff --git a/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts b/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts index f1d5834c5a319c..6e2621f7c6cb40 100644 --- a/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts +++ b/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts @@ -1,14 +1,14 @@ import prismock from "../../../../../../tests/libs/__mocks__/prisma"; - -import { describe, expect, it, vi, beforeEach } from "vitest"; - +import { intentToCreateOrgHandler } from "./intentToCreateOrg.handler"; import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService"; import { OrganizationPaymentService } from "@calcom/features/ee/organizations/lib/OrganizationPaymentService"; -import { BillingPeriod, UserPermissionRole, CreationSource } from "@calcom/prisma/enums"; - +import { + BillingPeriod, + UserPermissionRole, + CreationSource, +} from "@calcom/prisma/enums"; import { TRPCError } from "@trpc/server"; - -import { intentToCreateOrgHandler } from "./intentToCreateOrg.handler"; +import { describe, expect, it, vi, beforeEach } from "vitest"; vi.mock("@calcom/ee/common/server/LicenseKeyService", () => ({ LicenseKeySingleton: { @@ -18,6 +18,65 @@ vi.mock("@calcom/ee/common/server/LicenseKeyService", () => ({ vi.mock("@calcom/features/ee/organizations/lib/OrganizationPaymentService"); +vi.mock("@calcom/features/ee/teams/repositories/TeamRepository", () => ({ + TeamRepository: class { + constructor() {} + findOwnedTeamsByUserId() { + return Promise.resolve([]); + } + findById() { + return Promise.resolve({ + id: 1, + name: "Test Org", + slug: "test-org", + logoUrl: null, + parentId: null, + metadata: {}, + isOrganization: true, + organizationSettings: null, + isPlatform: false, + }); + } + }, +})); + +vi.mock( + "@calcom/trpc/server/routers/viewer/organizations/createTeams.handler", + () => ({ + createTeamsHandler: vi.fn().mockResolvedValue({}), + }) +); + +vi.mock( + "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler", + () => ({ + inviteMembersWithNoInviterPermissionCheck: vi.fn().mockResolvedValue({}), + }) +); + +vi.mock("@calcom/lib/server/i18n", () => ({ + getTranslation: vi + .fn() + .mockImplementation(async (locale: string, namespace: string) => { + const t = (key: string) => key; + t.locale = locale; + t.namespace = namespace; + return t; + }), +})); + +vi.mock("@calcom/lib/domainManager/organization", () => ({ + createDomain: vi.fn().mockResolvedValue({}), +})); + +vi.mock("@calcom/emails/organization-email-service", () => ({ + sendOrganizationCreationEmail: vi.fn().mockResolvedValue({}), +})); + +vi.mock("@calcom/features/auth/lib/verifyEmail", () => ({ + sendEmailVerification: vi.fn().mockResolvedValue({}), +})); + const mockInput = { name: "Test Org", slug: "test-org", @@ -57,25 +116,27 @@ describe("intentToCreateOrgHandler", () => { vi.mocked(OrganizationPaymentService).mockImplementation(() => { return { - createOrganizationOnboarding: vi.fn().mockImplementation(async (data: any) => { - return await prismock.organizationOnboarding.create({ - data: { - id: "onboarding-123", - name: data.name, - slug: data.slug, - orgOwnerEmail: data.orgOwnerEmail, - seats: data.seats ?? 10, - pricePerSeat: data.pricePerSeat ?? 15, - billingPeriod: data.billingPeriod ?? BillingPeriod.MONTHLY, - isComplete: false, - stripeCustomerId: null, - createdById: data.createdByUserId, - teams: data.teams ?? [], - invitedMembers: data.invitedMembers ?? [], - isPlatform: data.isPlatform ?? false, - }, - }); - }), + createOrganizationOnboarding: vi + .fn() + .mockImplementation(async (data: any) => { + return await prismock.organizationOnboarding.create({ + data: { + id: "onboarding-123", + name: data.name, + slug: data.slug, + orgOwnerEmail: data.orgOwnerEmail, + seats: data.seats ?? 10, + pricePerSeat: data.pricePerSeat ?? 15, + billingPeriod: data.billingPeriod ?? BillingPeriod.MONTHLY, + isComplete: false, + stripeCustomerId: null, + createdById: data.createdByUserId, + teams: data.teams ?? [], + invitedMembers: data.invitedMembers ?? [], + isPlatform: data.isPlatform ?? false, + }, + }); + }), createPaymentIntent: vi.fn().mockResolvedValue({ checkoutUrl: "https://stripe.com/checkout/session", organizationOnboarding: {}, @@ -137,20 +198,25 @@ describe("intentToCreateOrgHandler", () => { organizationOnboardingId: expect.any(String), checkoutUrl: null, organizationId: null, // Not created yet - handover flow - handoverUrl: expect.stringContaining("/settings/organizations/new/resume?onboardingId="), + handoverUrl: expect.stringContaining( + "/settings/organizations/new/resume?onboardingId=" + ), }); // Verify organization onboarding was created - const organizationOnboarding = await prismock.organizationOnboarding.findFirst({ - where: { - slug: mockInput.slug, - }, - }); + const organizationOnboarding = + await prismock.organizationOnboarding.findFirst({ + where: { + slug: mockInput.slug, + }, + }); expect(organizationOnboarding).toBeDefined(); expect(organizationOnboarding?.name).toBe(mockInput.name); expect(organizationOnboarding?.slug).toBe(mockInput.slug); - expect(organizationOnboarding?.orgOwnerEmail).toBe(mockInput.orgOwnerEmail); + expect(organizationOnboarding?.orgOwnerEmail).toBe( + mockInput.orgOwnerEmail + ); }); it("should allow user to create org for themselves", async () => { @@ -185,7 +251,12 @@ describe("intentToCreateOrgHandler", () => { user: null as any, }, }) - ).rejects.toThrow(new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized." })); + ).rejects.toThrow( + new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized.", + }) + ); }); it("should throw forbidden error when non-admin tries to create org for another user", async () => { @@ -344,20 +415,25 @@ describe("intentToCreateOrgHandler", () => { organizationOnboardingId: expect.any(String), checkoutUrl: null, organizationId: null, // Not created yet - handover flow - handoverUrl: expect.stringContaining("/settings/organizations/new/resume?onboardingId="), + handoverUrl: expect.stringContaining( + "/settings/organizations/new/resume?onboardingId=" + ), }); // Verify organization onboarding was created - const organizationOnboarding = await prismock.organizationOnboarding.findFirst({ - where: { - slug: mockInput.slug, - }, - }); + const organizationOnboarding = + await prismock.organizationOnboarding.findFirst({ + where: { + slug: mockInput.slug, + }, + }); expect(organizationOnboarding).toBeDefined(); expect(organizationOnboarding?.name).toBe(mockInput.name); expect(organizationOnboarding?.slug).toBe(mockInput.slug); - expect(organizationOnboarding?.orgOwnerEmail).toBe(mockInput.orgOwnerEmail); + expect(organizationOnboarding?.orgOwnerEmail).toBe( + mockInput.orgOwnerEmail + ); }); it("should handle teams and invites in the request", async () => { @@ -397,15 +473,20 @@ describe("intentToCreateOrgHandler", () => { expect(result.organizationOnboardingId).toBeDefined(); - const organizationOnboarding = await prismock.organizationOnboarding.findFirst({ - where: { - slug: mockInput.slug, - }, - }); + const organizationOnboarding = + await prismock.organizationOnboarding.findFirst({ + where: { + slug: mockInput.slug, + }, + }); expect(organizationOnboarding).toBeDefined(); - expect(organizationOnboarding?.teams).toEqual(inputWithTeamsAndInvites.teams); - expect(organizationOnboarding?.invitedMembers).toEqual(inputWithTeamsAndInvites.invitedMembers); + expect(organizationOnboarding?.teams).toEqual( + inputWithTeamsAndInvites.teams + ); + expect(organizationOnboarding?.invitedMembers).toEqual( + inputWithTeamsAndInvites.invitedMembers + ); }); it("should preserve teamName, teamId, and role in invites payload", async () => { @@ -433,7 +514,12 @@ describe("intentToCreateOrgHandler", () => { ], invitedMembers: [ { email: "new@new.com", teamName: "new", teamId: -1, role: "ADMIN" }, - { email: "team@new.com", teamName: "team", teamId: -1, role: "ADMIN" }, + { + email: "team@new.com", + teamName: "team", + teamId: -1, + role: "ADMIN", + }, ], }; @@ -446,14 +532,17 @@ describe("intentToCreateOrgHandler", () => { expect(result.organizationOnboardingId).toBeDefined(); - const organizationOnboarding = await prismock.organizationOnboarding.findFirst({ - where: { - slug: mockInput.slug, - }, - }); + const organizationOnboarding = + await prismock.organizationOnboarding.findFirst({ + where: { + slug: mockInput.slug, + }, + }); expect(organizationOnboarding).toBeDefined(); - expect(organizationOnboarding?.teams).toEqual(inputWithTeamsAndInvites.teams); + expect(organizationOnboarding?.teams).toEqual( + inputWithTeamsAndInvites.teams + ); // Verify invitedMembers are stored with all fields including teamName, teamId, and role expect(organizationOnboarding?.invitedMembers).toBeDefined();