diff --git a/.easignore b/.easignore new file mode 100644 index 00000000000000..fd9cae677a52ea --- /dev/null +++ b/.easignore @@ -0,0 +1,57 @@ +# EAS Build ignore file for monorepo +# Only upload the companion app folder + +# =========================================== +# IGNORE EVERYTHING AT ROOT LEVEL +# =========================================== +/* + +# =========================================== +# EXCEPT THE COMPANION APP +# =========================================== +!companion + +# =========================================== +# BUT IGNORE THESE INSIDE COMPANION +# =========================================== + +# Native folders - EAS generates these during builds (~2.1 GB) +companion/ios +companion/android + +# Dependencies - EAS installs these during builds (~1.6 GB) +companion/node_modules + +# Expo build cache +companion/.expo +companion/dist +companion/web-build + +# Metro +companion/.metro-health-check* + +# WXT Chrome Extension outputs (not needed for mobile builds) +companion/.output +companion/.wxt +companion/dev + +# Kotlin cache +companion/.kotlin + +# Build artifacts +companion/*.jks +companion/*.p8 +companion/*.p12 +companion/*.key +companion/*.mobileprovision + +# Debug logs +companion/*.log + +# TypeScript build info +companion/*.tsbuildinfo + +# Environment files +companion/.env +companion/.env.* +!companion/.env.example diff --git a/.github/actions/yarn-install/action.yml b/.github/actions/yarn-install/action.yml index a930ce326d37ef..e30334a1c94efe 100644 --- a/.github/actions/yarn-install/action.yml +++ b/.github/actions/yarn-install/action.yml @@ -112,9 +112,7 @@ runs: - 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 - yarn prisma generate + run: yarn install --inline-builds env: # CI optimizations. Overrides yarnrc.yml options (or their defaults) in the CI action. YARN_ENABLE_IMMUTABLE_INSTALLS: "false" # So it doesn't try to remove our private submodule deps diff --git a/.github/workflows/api-v2-unit-tests.yml b/.github/workflows/api-v2-unit-tests.yml index 20d599ccd83101..5c6fd7b69c4e5b 100644 --- a/.github/workflows/api-v2-unit-tests.yml +++ b/.github/workflows/api-v2-unit-tests.yml @@ -14,6 +14,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install + - run: yarn prisma generate - name: Run API v2 unit tests working-directory: apps/api/v2 run: | diff --git a/.github/workflows/atoms-production-build.yml b/.github/workflows/atoms-production-build.yml index 6094722d764c23..b8fc029accedc3 100644 --- a/.github/workflows/atoms-production-build.yml +++ b/.github/workflows/atoms-production-build.yml @@ -20,17 +20,11 @@ jobs: env: cache-name: atoms-build key-1: ${{ hashFiles('yarn.lock') }} - key-2: ${{ hashFiles('packages/platform/atoms/**.[jt]s', 'packages/platform/atoms/**.[jt]sx', '!**/node_modules') }} - key-3: ${{ github.event.pull_request.number || github.ref }} - # Ensures production-build.yml will always be fresh - key-4: ${{ github.sha }} + key-2: ${{ hashFiles('packages/platform/atoms/**.[jt]s', 'packages/platform/atoms/**.[jt]sx', 'packages/platform/atoms/package.json', '!**/node_modules') }} with: path: | **/dist/** - key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }} - - name: Log Cache Hit - if: steps.cache-atoms-build.outputs.cache-hit == 'true' - run: echo "Cache hit for Atoms build. Skipping build." + key: ${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }} - name: Run build if: steps.cache-atoms-build.outputs.cache-hit != 'true' run: | diff --git a/.github/workflows/check-api-v2-breaking-changes.yml b/.github/workflows/check-api-v2-breaking-changes.yml index cc124fc538c9f2..b41d3b719b7bff 100644 --- a/.github/workflows/check-api-v2-breaking-changes.yml +++ b/.github/workflows/check-api-v2-breaking-changes.yml @@ -39,7 +39,9 @@ jobs: - name: Generate Swagger working-directory: apps/api/v2 - run: yarn generate-swagger + run: | + yarn prisma generate + yarn generate-swagger - name: Check API v2 breaking changes run: | diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index abecee2f75d552..ccf44ed8a6ac46 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -20,16 +20,10 @@ jobs: cache-name: docs-build key-1: ${{ hashFiles('yarn.lock') }} key-2: ${{ hashFiles('docs/**.*', '!**/node_modules') }} - key-3: ${{ github.event.pull_request.number || github.ref }} - key-4: ${{ github.sha }} with: path: | **/docs/** - key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }} - # Log cache hit - - name: Log Cache Hit - if: steps.cache-docs-build.outputs.cache-hit == 'true' - run: echo "Cache hit for Docs build. Skipping build." + key: ${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }} - name: Run build if: steps.cache-docs-build.outputs.cache-hit != 'true' working-directory: docs diff --git a/.github/workflows/e2e-app-store.yml b/.github/workflows/e2e-app-store.yml index 8a4ca30454f3ec..e3e1774c805d28 100644 --- a/.github/workflows/e2e-app-store.yml +++ b/.github/workflows/e2e-app-store.yml @@ -84,6 +84,7 @@ jobs: - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install - uses: ./.github/actions/yarn-playwright-install + - run: yarn prisma generate - uses: ./.github/actions/cache-db env: DATABASE_URL: ${{ secrets.CI_DATABASE_URL }} diff --git a/.github/workflows/e2e-embed-react.yml b/.github/workflows/e2e-embed-react.yml index 80112c3e4b4dd1..a58cac84840255 100644 --- a/.github/workflows/e2e-embed-react.yml +++ b/.github/workflows/e2e-embed-react.yml @@ -76,6 +76,7 @@ jobs: - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install - uses: ./.github/actions/yarn-playwright-install + - run: yarn prisma generate - uses: ./.github/actions/cache-db - uses: ./.github/actions/cache-build - name: Run Tests diff --git a/.github/workflows/e2e-embed.yml b/.github/workflows/e2e-embed.yml index 614b098d935010..c3b9b5e3c642ea 100644 --- a/.github/workflows/e2e-embed.yml +++ b/.github/workflows/e2e-embed.yml @@ -84,6 +84,7 @@ jobs: - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install - uses: ./.github/actions/yarn-playwright-install + - run: yarn prisma generate - uses: ./.github/actions/cache-db - uses: ./.github/actions/cache-build - name: Run Tests diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a5132a89d11419..54a20cb066ce53 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -85,6 +85,7 @@ jobs: - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install - uses: ./.github/actions/yarn-playwright-install + - run: yarn prisma generate - uses: ./.github/actions/cache-db - uses: ./.github/actions/cache-build - name: Run Tests diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index c9c6563e88d29b..83605ea5ccba94 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -81,6 +81,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install + - run: yarn prisma generate - uses: ./.github/actions/cache-db - name: Run Tests run: VITEST_MODE=integration yarn test diff --git a/.github/workflows/run-ci.yml b/.github/workflows/run-ci.yml index a82c106d362cfe..452de59ab5e111 100644 --- a/.github/workflows/run-ci.yml +++ b/.github/workflows/run-ci.yml @@ -11,7 +11,7 @@ permissions: jobs: trigger: name: Trigger CI - if: github.event.label.name == 'run-ci' + if: github.event.label.name == 'run-ci' || github.event.label.name == 'ready-for-e2e' runs-on: ubuntu-latest steps: - name: Verify and trigger CI diff --git a/.github/workflows/setup-db.yml b/.github/workflows/setup-db.yml index a4618d1fb92141..f0634a0c68502e 100644 --- a/.github/workflows/setup-db.yml +++ b/.github/workflows/setup-db.yml @@ -50,5 +50,7 @@ jobs: - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install if: inputs.DB_CACHE_HIT != 'true' + - run: yarn prisma generate + if: inputs.DB_CACHE_HIT != 'true' - uses: ./.github/actions/cache-db if: inputs.DB_CACHE_HIT != 'true' diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 34fd4c1d086bd7..a922ec9698aaad 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -14,6 +14,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install + - run: yarn prisma generate - run: yarn test -- --no-isolate # We could add different timezones here that we need to run our tests in - run: TZ=America/Los_Angeles VITEST_MODE=timezone yarn test -- --no-isolate diff --git a/.husky/pre-commit b/.husky/pre-commit index bbf4dec8aba98c..1d4d20d67fa860 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,3 +1,7 @@ -yarn lint-staged +if [ -f .git/MERGE_HEAD ]; then + echo "Merge detected. Skipping lint-staged and generators." + exit 0 +fi +yarn lint-staged yarn app-store:build && git add packages/app-store/*.generated.* diff --git a/apps/web/components/apps/routing-forms/TestFormDialog.test.tsx b/apps/web/components/apps/routing-forms/TestFormDialog.test.tsx index 5f2b366b83e0f7..1dbeec8d5bada4 100644 --- a/apps/web/components/apps/routing-forms/TestFormDialog.test.tsx +++ b/apps/web/components/apps/routing-forms/TestFormDialog.test.tsx @@ -1,6 +1,6 @@ -import { render, screen, fireEvent } from "@testing-library/react"; +import { render, screen, fireEvent, cleanup } from "@testing-library/react"; import type { Mock } from "vitest"; -import { vi } from "vitest"; +import { vi, beforeEach, afterEach, describe, expect, it } from "vitest"; import { findMatchingRoute } from "@calcom/app-store/routing-forms/lib/processRoute"; @@ -194,6 +194,15 @@ describe("TestFormDialog", () => { beforeEach(() => { resetFindTeamMembersMatchingAttributeLogicResponse(); vi.clearAllMocks(); + vi.useFakeTimers(); + }); + + afterEach(() => { + // Flush any pending timers (like Radix FocusScope setTimeout) before cleanup + // to prevent them from firing after jsdom teardown + vi.runOnlyPendingTimers(); + vi.useRealTimers(); + cleanup(); }); it("renders the dialog when open", () => { diff --git a/apps/web/components/booking/__tests__/CancelBooking.cancellationFee.test.tsx b/apps/web/components/booking/__tests__/CancelBooking.cancellationFee.test.tsx index d61226b683795d..ce37d66d7768da 100644 --- a/apps/web/components/booking/__tests__/CancelBooking.cancellationFee.test.tsx +++ b/apps/web/components/booking/__tests__/CancelBooking.cancellationFee.test.tsx @@ -1,15 +1,40 @@ -import { render, screen } from "@testing-library/react"; -import * as React from "react"; -import { describe, expect, it, vi, beforeAll } from "vitest"; +import { render, screen, cleanup } from "@testing-library/react"; +import { describe, expect, it, vi, beforeAll, afterAll, afterEach } from "vitest"; import * as shouldChargeModule from "@calcom/features/bookings/lib/payment/shouldChargeNoShowCancellationFee"; import CancelBooking from "../CancelBooking"; +// Mock the embed-iframe module to prevent it from scheduling timers/RAF that can cause +// teardown issues when jsdom environment is destroyed +vi.mock("@calcom/embed-core/embed-iframe", () => ({ + sdkActionManager: null, +})); + +// Store original scrollIntoView to restore later +const originalScrollIntoView = Element.prototype.scrollIntoView; + beforeAll(() => { + // jsdom doesn't implement scrollIntoView, so we need to mock it Element.prototype.scrollIntoView = vi.fn(); }); +afterAll(() => { + // Restore scrollIntoView to avoid polluting other tests in the same worker + if (originalScrollIntoView) { + Element.prototype.scrollIntoView = originalScrollIntoView; + } else { + // If it was originally undefined, delete it + delete (Element.prototype as { scrollIntoView?: unknown }).scrollIntoView; + } + // Clean up module mocks to avoid polluting other tests + vi.unmock("@calcom/embed-core/embed-iframe"); +}); + +afterEach(() => { + cleanup(); +}); + vi.mock("@calcom/trpc/react", () => ({ trpc: { viewer: { diff --git a/apps/web/playwright/booking-pages.e2e.ts b/apps/web/playwright/booking-pages.e2e.ts index f38e672c5020ca..aab886487cb210 100644 --- a/apps/web/playwright/booking-pages.e2e.ts +++ b/apps/web/playwright/booking-pages.e2e.ts @@ -577,16 +577,9 @@ test.describe("Booking on different layouts", () => { await page.click('[data-testid="toggle-group-item-column_view"]'); - 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(); + // Use the standard helper to select an available time slot next month + // This is more robust than manually clicking incrementMonth and reloading + await selectFirstAvailableTimeSlotNextMonth(page); // Fill what is this meeting about? name email and notes await page.locator('[name="name"]').fill("Test name"); diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index e530d1fd574256..4a53966ec61ed4 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -1106,17 +1106,49 @@ export async function login( await responsePromise; } +/** + * Helper to retry network requests that may fail with transient errors like ECONNRESET + */ +async function retryOnNetworkError( + fn: () => Promise, + maxRetries = 3, + delayMs = 500 +): Promise { + let lastError: Error | undefined; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (error) { + lastError = error as Error; + const errorMessage = lastError.message || ""; + // Only retry on transient network errors + const isRetryable = + errorMessage.includes("ECONNRESET") || + errorMessage.includes("ECONNREFUSED") || + errorMessage.includes("ETIMEDOUT") || + errorMessage.includes("socket hang up"); + + if (!isRetryable || attempt === maxRetries) { + throw lastError; + } + // Wait before retrying with exponential backoff + await new Promise((resolve) => setTimeout(resolve, delayMs * attempt)); + } + } + throw lastError; +} + export async function apiLogin( user: Pick & Partial> & { password: string | null }, page: Page, navigateToUrl?: string ) { - // Get CSRF token - const csrfToken = await page - .context() - .request.get("/api/auth/csrf") - .then((response) => response.json()) - .then((json) => json.csrfToken); + // Get CSRF token with retry for transient network errors + const csrfToken = await retryOnNetworkError(async () => { + const response = await page.context().request.get("/api/auth/csrf"); + const json = await response.json(); + return json.csrfToken; + }); // Make the login request const loginData = { @@ -1128,9 +1160,11 @@ export async function apiLogin( csrfToken, }; - const response = await page.context().request.post("/api/auth/callback/credentials", { - data: loginData, - }); + const response = await retryOnNetworkError(() => + page.context().request.post("/api/auth/callback/credentials", { + data: loginData, + }) + ); expect(response.status()).toBe(200); diff --git a/apps/web/playwright/lib/testUtils.ts b/apps/web/playwright/lib/testUtils.ts index 98f0a61d73a673..665edf32044ab1 100644 --- a/apps/web/playwright/lib/testUtils.ts +++ b/apps/web/playwright/lib/testUtils.ts @@ -416,19 +416,32 @@ export async function fillStripeTestCheckout(page: Page) { export function goToUrlWithErrorHandling({ page, url }: { page: Page; url: string }) { return new Promise<{ success: boolean; url: string }>(async (resolve) => { + let resolved = false; const onRequestFailed = (request: PlaywrightRequest) => { + // Only consider it a navigation failure if it's the main document request + // Ignore failures for subresources like images, scripts, RSC requests, etc. + if (!request.isNavigationRequest() || request.frame() !== page.mainFrame()) { + const failedToLoadUrl = request.url(); + console.log("goToUrlWithErrorHandling: Failed to load URL:", failedToLoadUrl); + return; + } + if (resolved) return; + resolved = true; const failedToLoadUrl = request.url(); - console.log("goToUrlWithErrorHandling: Failed to load URL:", failedToLoadUrl); + console.log("goToUrlWithErrorHandling: Navigation failed for URL:", failedToLoadUrl); resolve({ success: false, url: failedToLoadUrl }); }; page.on("requestfailed", onRequestFailed); try { - await page.goto(url); + await page.goto(url, { waitUntil: "domcontentloaded" }); } catch { // do nothing } page.off("requestfailed", onRequestFailed); - resolve({ success: true, url: page.url() }); + if (!resolved) { + resolved = true; + resolve({ success: true, url: page.url() }); + } }); } diff --git a/apps/web/playwright/teams.e2e.ts b/apps/web/playwright/teams.e2e.ts index 6bc77519cd7af6..376fe4ba785736 100644 --- a/apps/web/playwright/teams.e2e.ts +++ b/apps/web/playwright/teams.e2e.ts @@ -171,8 +171,11 @@ test.describe("Teams - NonOrg", () => { // eslint-disable-next-line playwright/no-conditional-in-test if (IS_TEAM_BILLING_ENABLED) await fillStripeTestCheckout(page); await page.waitForURL(/\/settings\/teams\/(\d+)\/onboard-members.*$/i); - // Click text=Continue - await page.locator("[data-testid=publish-button]").click(); + // Wait for the page to fully load and the publish button to be visible + await page.waitForLoadState("networkidle"); + const publishButton = page.locator("[data-testid=publish-button]"); + await publishButton.waitFor({ state: "visible", timeout: 10000 }); + await publishButton.click(); await page.waitForURL(/\/settings\/teams\/(\d+)\/event-type*$/i); await page.locator("[data-testid=handle-later-button]").click(); await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i); diff --git a/apps/web/playwright/unpublished.e2e.ts b/apps/web/playwright/unpublished.e2e.ts index f52fc5fd491c43..ef0bf00b099057 100644 --- a/apps/web/playwright/unpublished.e2e.ts +++ b/apps/web/playwright/unpublished.e2e.ts @@ -4,16 +4,13 @@ import { SchedulingType } from "@calcom/prisma/enums"; import { test } from "./lib/fixtures"; +// Keep parallel mode - each test creates its own isolated data test.describe.configure({ mode: "parallel" }); const title = (name: string) => `${name} is unpublished`; const description = (entity: string) => `This ${entity} link is currently not available. Please contact the ${entity} owner or ask them to publish it.`; -test.afterEach(async ({ users }) => { - await users.deleteAll(); -}); - const assertChecks = async (page: any, entityName: string, entityType: string) => { await expect(page.locator('[data-testid="empty-screen"]')).toHaveCount(1); await expect(page.locator(`h2:has-text("${title(entityName)}")`)).toHaveCount(1); @@ -21,20 +18,13 @@ const assertChecks = async (page: any, entityName: string, entityType: string) = await expect(page.locator(`img`)).toHaveAttribute("src", /.*/); }; -test.describe("Unpublished", () => { - test("Regular team profile", async ({ page, users }) => { - const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true }); - const { team } = await owner.getFirstTeamMembership(); - const { requestedSlug } = team.metadata as { requestedSlug: string }; - const prefixes = ["", "/en"]; - - for (const prefix of prefixes) { - await page.goto(`${prefix}/team/${requestedSlug}`); - await assertChecks(page, team.name, "team"); - } +// Group 1: Regular team tests - share setup data +test.describe("Unpublished - Regular team", () => { + test.afterEach(async ({ users }) => { + await users.deleteAll(); }); - test("Regular team event type", async ({ page, users }) => { + test("Regular team profile", async ({ page, users }) => { const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, @@ -42,46 +32,63 @@ test.describe("Unpublished", () => { }); const { team } = await owner.getFirstTeamMembership(); const { requestedSlug } = team.metadata as { requestedSlug: string }; - const { slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id); + const teamEventSlug = (await owner.getFirstTeamEvent(team.id)).slug; const prefixes = ["", "/en"]; + // Test team profile + for (const prefix of prefixes) { + await page.goto(`${prefix}/team/${requestedSlug}`); + await assertChecks(page, team.name, "team"); + } + + // Test team event type (reuse same data) for (const prefix of prefixes) { await page.goto(`${prefix}/team/${requestedSlug}/${teamEventSlug}`); await assertChecks(page, team.name, "team"); } }); +}); + +// Group 2: Organization tests - share setup data +test.describe("Unpublished - Organization", () => { + test.afterEach(async ({ users }) => { + await users.deleteAll(); + }); - test("Organization profile", async ({ users, page }) => { + test("Organization profile and user", async ({ users, page }) => { const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true }); const { team: org } = await owner.getOrgMembership(); const { requestedSlug } = org.metadata as { requestedSlug: string }; + const [{ slug: ownerEventType }] = owner.eventTypes; const prefixes = ["", "/en"]; + // Test organization profile for (const prefix of prefixes) { await page.goto(`${prefix}/org/${requestedSlug}`); await assertChecks(page, org.name, "organization"); } - }); - test("Organization sub-team", async ({ users, page }) => { - const owner = await users.create(undefined, { - hasTeam: true, - isUnpublished: true, - isOrg: true, - hasSubteam: true, - }); - const { team: org } = await owner.getOrgMembership(); - const { requestedSlug } = org.metadata as { requestedSlug: string }; - const [{ slug: subteamSlug }] = org.children as { slug: string }[]; - const prefixes = ["", "/en"]; + // Test organization user + for (const prefix of prefixes) { + await page.goto(`${prefix}/org/${requestedSlug}/${owner.username}`); + await assertChecks(page, org.name, "organization"); + } + // Test organization user event-type for (const prefix of prefixes) { - await page.goto(`${prefix}/org/${requestedSlug}/team/${subteamSlug}`); + await page.goto(`${prefix}/org/${requestedSlug}/${owner.username}/${ownerEventType}`); await assertChecks(page, org.name, "organization"); } }); +}); + +// Group 3: Organization sub-team tests - share setup data +test.describe("Unpublished - Organization sub-team", () => { + test.afterEach(async ({ users }) => { + await users.deleteAll(); + }); - test("Organization sub-team event-type", async ({ users, page }) => { + test("Organization sub-team and event-type", async ({ users, page }) => { const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, @@ -94,33 +101,15 @@ test.describe("Unpublished", () => { const { slug: subteamEventSlug } = await owner.getFirstTeamEvent(subteamId); const prefixes = ["", "/en"]; + // Test organization sub-team for (const prefix of prefixes) { - await page.goto(`${prefix}/org/${requestedSlug}/team/${subteamSlug}/${subteamEventSlug}`); - await assertChecks(page, org.name, "organization"); - } - }); - - test("Organization user", async ({ users, page }) => { - const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true }); - const { team: org } = await owner.getOrgMembership(); - const { requestedSlug } = org.metadata as { requestedSlug: string }; - const prefixes = ["", "/en"]; - - for (const prefix of prefixes) { - await page.goto(`${prefix}/org/${requestedSlug}/${owner.username}`); + await page.goto(`${prefix}/org/${requestedSlug}/team/${subteamSlug}`); await assertChecks(page, org.name, "organization"); } - }); - - test("Organization user event-type", async ({ users, page }) => { - const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true }); - const { team: org } = await owner.getOrgMembership(); - const { requestedSlug } = org.metadata as { requestedSlug: string }; - const [{ slug: ownerEventType }] = owner.eventTypes; - const prefixes = ["", "/en"]; + // Test organization sub-team event-type for (const prefix of prefixes) { - await page.goto(`${prefix}/org/${requestedSlug}/${owner.username}/${ownerEventType}`); + await page.goto(`${prefix}/org/${requestedSlug}/team/${subteamSlug}/${subteamEventSlug}`); await assertChecks(page, org.name, "organization"); } }); diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.test.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.test.tsx deleted file mode 100644 index f21af3756ffd0a..00000000000000 --- a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.test.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import { TooltipProvider } from "@radix-ui/react-tooltip"; -import { render, fireEvent, screen } from "@testing-library/react"; -import * as React from "react"; -import type { UseFormReturn } from "react-hook-form"; -import { FormProvider, useForm } from "react-hook-form"; -import { expect, vi } from "vitest"; - -import PhoneInput from "@calcom/features/components/phone-input/PhoneInput"; - -import { getBookingFieldsWithSystemFields } from "../../../lib/getBookingFields"; -import { BookingFields } from "./BookingFields"; - -// Mock PhoneInput to avoid calling the lazy import -vi.mock("@calcom/features/components/phone-input", () => { - return { - default: PhoneInput, - }; -}); - -vi.mock("@calcom/ui/components/address", async (originalImport) => { - const { AddressInputNonLazy } = (await originalImport()) as Record; - // Dynamic imports of Components are not supported in Vitest. So, we use the non-lazy version of the components - return { - AddressInput: AddressInputNonLazy, - }; -}); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type FormMethods = UseFormReturn; - -// Add tRPC mock before tests -vi.mock("@calcom/trpc/react", () => ({ - trpc: { - viewer: { - public: { - countryCode: { - useQuery: () => ({ - data: { countryCode: "US" }, - isLoading: false, - error: null, - }), - }, - }, - }, - }, -})); - -const renderComponent = ({ - props: props, - formDefaultValues, -}: { - props: Parameters[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - formDefaultValues?: any; -}) => { - let formMethods: UseFormReturn | undefined; - const Wrapper = ({ children }: { children: React.ReactNode }) => { - const form = useForm({ - defaultValues: formDefaultValues, - }); - formMethods = form; - return ( - - {children} - - ); - }; - const result = render(, { wrapper: Wrapper }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return { result, formMethods: formMethods! }; -}; - -describe("BookingFields", () => { - it("should correctly render with location fields", () => { - const AttendeePhoneNumberOption = { - label: "attendee_phone_number", - value: "phone", - }; - - const OrganizerLinkOption = { - label: "https://google.com", - value: "link", - }; - - const locations = [ - { - type: AttendeePhoneNumberOption.value, - }, - { - link: "https://google.com", - type: OrganizerLinkOption.value, - displayLocationPublicly: true, - }, - ]; - const { formMethods } = renderComponent({ - props: { - fields: getBookingFieldsWithSystemFields({ - disableGuests: false, - bookingFields: [], - metadata: null, - workflows: [], - customInputs: [], - }), - locations, - isDynamicGroupBooking: false, - bookingData: null, - }, - formDefaultValues: {}, - }); - - component.fillName({ value: "John Doe" }); - component.fillEmail({ value: "john.doe@example.com" }); - component.fillNotes({ value: "This is a note" }); - expectScenarios.expectNameToBe({ value: "John Doe", formMethods }); - expectScenarios.expectEmailToBe({ value: "john.doe@example.com", formMethods }); - expectScenarios.expectNotesToBe({ value: "This is a note", formMethods }); - - component.fillRadioInputLocation({ label: AttendeePhoneNumberOption.label, inputValue: "+1234567890" }); - expectScenarios.expectLocationToBe({ - formMethods, - label: AttendeePhoneNumberOption.label, - toMatch: { - formattedValue: "+1 (234) 567-890", - value: { optionValue: "+1234567890", value: AttendeePhoneNumberOption.value }, - }, - }); - - component.fillRadioInputLocation({ label: OrganizerLinkOption.label }); - expectScenarios.expectLocationToBe({ - formMethods, - label: OrganizerLinkOption.label, - toMatch: { - formattedValue: "+1 (234) 567-890", - value: { optionValue: "", value: OrganizerLinkOption.value }, - }, - }); - }); -}); - -const component = { - getName: ({ label = "your_name" }: { label?: string } = {}) => - screen.getByRole("textbox", { - name: new RegExp(label), - }) as HTMLInputElement, - getEmail: () => screen.getByRole("textbox", { name: /email/i }) as HTMLInputElement, - getLocationRadioOption: ({ label }: { label: string }) => - screen.getByRole("radio", { name: new RegExp(label) }) as HTMLInputElement, - getLocationRadioInput: ({ placeholder }: { placeholder: string }) => - screen.getByPlaceholderText(placeholder) as HTMLInputElement, - getNotes: () => screen.getByRole("textbox", { name: /additional_notes/i }) as HTMLInputElement, - getGuests: () => screen.getByLabelText("guests"), - fillName: ({ value }: { value: string }) => { - fireEvent.change(component.getName(), { target: { value } }); - }, - fillEmail: ({ value }: { value: string }) => { - fireEvent.change(component.getEmail(), { target: { value } }); - }, - fillRadioInputLocation: ({ label, inputValue }: { label: string; inputValue?: string }) => { - fireEvent.click(component.getLocationRadioOption({ label })); - - if (inputValue) { - let placeholder = label; - if (label === "attendee_phone_number") { - placeholder = "enter_phone_number"; - } else { - // radioInput doesn't have a label, so we need to identify by placeholder - throw new Error("Tell me how to identify the placeholder for this location input"); - } - fireEvent.change(component.getLocationRadioInput({ placeholder }), { - target: { value: inputValue }, - }); - } - }, - fillNotes: ({ value }: { value: string }) => { - fireEvent.change(component.getNotes(), { target: { value } }); - }, -}; - -const expectScenarios = { - expectNameToBe: ({ value, formMethods }: { value: string; formMethods: FormMethods }) => { - expect(component.getName().value).toEqual(value); - expect(formMethods.getValues("responses.name")).toEqual(value); - }, - expectEmailToBe: ({ value, formMethods }: { value: string; formMethods: FormMethods }) => { - expect(component.getEmail().value).toEqual(value); - expect(formMethods.getValues("responses.email")).toEqual(value); - }, - expectLocationToBe: ({ - formMethods, - label, - toMatch: { formattedValue, value }, - }: { - label: string; - toMatch: { - formattedValue?: string; - value: { - optionValue: string; - value: string; - }; - }; - formMethods: FormMethods; - }) => { - expect(component.getLocationRadioOption({ label }).checked).toBe(true); - if (value.optionValue) { - expect(component.getLocationRadioInput({ placeholder: "enter_phone_number" }).value).toEqual( - formattedValue - ); - } - expect(formMethods.getValues("responses.location")).toEqual(value); - }, - expectNotesToBe: ({ value, formMethods }: { value: string; formMethods: FormMethods }) => { - expect(component.getNotes().value).toEqual(value); - expect(formMethods.getValues("responses.notes")).toEqual(value); - }, -}; diff --git a/packages/lib/crypto.test.ts b/packages/lib/crypto.test.ts index e323798eaf9185..498792ac360819 100644 --- a/packages/lib/crypto.test.ts +++ b/packages/lib/crypto.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; -import { symmetricEncrypt, symmetricDecrypt } from "./crypto"; +import { symmetricDecrypt, symmetricEncrypt } from "./crypto"; describe("crypto", () => { const testKey = "12345678901234567890123456789012"; // 32 bytes key @@ -49,11 +49,24 @@ describe("crypto", () => { expect(() => symmetricDecrypt(":", testKey)).toThrow(); }); - it("should throw error if wrong key is used", () => { + it("should fail to decrypt correctly if wrong key is used", () => { const encrypted = symmetricEncrypt(testText, testKey); const wrongKey = "12345678901234567890123456789013"; // Different 32 bytes key - expect(() => symmetricDecrypt(encrypted, wrongKey)).toThrow(); + // AES-256-CBC doesn't guarantee throwing on wrong key - it depends on whether + // the decrypted bytes happen to have valid PKCS#7 padding. The test verifies + // that decryption either throws OR returns a value different from the original. + let decryptedWithWrongKey: string | null = null; + let threwError = false; + + try { + decryptedWithWrongKey = symmetricDecrypt(encrypted, wrongKey); + } catch { + threwError = true; + } + + // Either it threw an error, or the decrypted value is not the original text + expect(threwError || decryptedWithWrongKey !== testText).toBe(true); }); it("should handle empty string encryption/decryption", () => { diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.test.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.test.ts index f7da466cfe78d6..62a9444f3f1ab4 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.test.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.test.ts @@ -1,13 +1,11 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ // @ts-nocheck // TODO: Bring this test back with the correct setup (no illegal imports) +// NOTE: All imports except vitest are deferred to inside the skipped describe blocks +// to prevent module loading side effects during test collection (which can cause +// "Closing rpc while fetch was pending" errors from Salesforce GraphQL module imports) import { describe, beforeEach, vi, expect, test } from "vitest"; -import { BookingStatus } from "@calcom/prisma/enums"; - -import type { TrpcSessionUser } from "../../../types"; -import { confirmHandler } from "./confirm.handler"; - //eslint-disable-next-line playwright/no-skipped-test describe.skip("confirmHandler", () => { beforeEach(() => { diff --git a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.test.ts b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.test.ts index d0b53781c92ad7..d33680ce06c39f 100644 --- a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.test.ts +++ b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.test.ts @@ -1,24 +1,11 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ // @ts-nocheck // TODO: Bring this test back with the correct setup (no illegal imports) +// NOTE: All imports except vitest are deferred to inside the skipped describe blocks +// to prevent module loading side effects during test collection (which can cause +// "Closing rpc while fetch was pending" errors from watchlist module imports) import { describe, expect, test, vi, beforeEach } from "vitest"; -import { prisma } from "@calcom/prisma"; -import { BookingStatus } from "@calcom/prisma/enums"; - -import { - editLocationHandler, - getLocationForOrganizerDefaultConferencingAppInEvtFormat, - SystemError, - UserError, -} from "./editLocation.handler"; - -vi.mock("@calcom/prisma", () => { - return { - prisma: vi.fn(), - }; -}); - describe.skip("getLocationForOrganizerDefaultConferencingAppInEvtFormat", () => { const mockTranslate = vi.fn((key: string) => key); diff --git a/packages/ui/components/address/index.ts b/packages/ui/components/address/index.ts index 09ecbf71f88bc6..03d621d09f2a2b 100644 --- a/packages/ui/components/address/index.ts +++ b/packages/ui/components/address/index.ts @@ -1,3 +1,2 @@ export { default as AddressInput } from "./AddressInputLazy"; export { default as MultiEmail } from "./MultiEmailLazy"; -export { default as AddressInputNonLazy } from "./AddressInput"; diff --git a/packages/ui/components/badge/UpgradeOrgsBadge.tsx b/packages/ui/components/badge/UpgradeOrgsBadge.tsx deleted file mode 100644 index 932cd82497cad8..00000000000000 --- a/packages/ui/components/badge/UpgradeOrgsBadge.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useLocale } from "@calcom/lib/hooks/useLocale"; - -import { Tooltip } from "../tooltip"; -import { Badge } from "./Badge"; - -export const UpgradeOrgsBadge = function UpgradeOrgsBadge() { - const { t } = useLocale(); - - return ( - - - {t("upgrade")} - - - ); -}; diff --git a/packages/ui/components/badge/index.ts b/packages/ui/components/badge/index.ts index b1ee3f44ef3e03..e46de5a423ed6a 100644 --- a/packages/ui/components/badge/index.ts +++ b/packages/ui/components/badge/index.ts @@ -1,6 +1,5 @@ export { Badge } from "./Badge"; export { UpgradeTeamsBadge } from "./UpgradeTeamsBadge"; export { CreditsBadge } from "./CreditsBadge"; -export { UpgradeOrgsBadge } from "./UpgradeOrgsBadge"; export { InfoBadge } from "./InfoBadge"; export type { BadgeProps } from "./Badge"; diff --git a/packages/ui/components/dropdown/index.ts b/packages/ui/components/dropdown/index.ts index 934ddb81c83468..f7dd2fec044c05 100644 --- a/packages/ui/components/dropdown/index.ts +++ b/packages/ui/components/dropdown/index.ts @@ -4,13 +4,9 @@ export { DropdownItem, DropdownMenuCheckboxItem, DropdownMenuContent, - DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, - DropdownMenuTriggerItem, } from "./Dropdown"; diff --git a/packages/ui/components/form/index.ts b/packages/ui/components/form/index.ts index bdd055ee30a4be..50ecec3624559e 100644 --- a/packages/ui/components/form/index.ts +++ b/packages/ui/components/form/index.ts @@ -1,12 +1,8 @@ export { Checkbox, MultiSelectCheckbox, CheckboxField } from "./checkbox"; export type { Option as MultiSelectCheckboxesOptionType } from "./checkbox"; -export { HintsOrErrors } from "./inputs/HintOrErrors"; export { EmailField, EmailInput, - FieldsetLegend, - InputGroupBox, - InputLeading, PasswordField, TextArea, TextAreaField, @@ -15,8 +11,6 @@ export { } from "./inputs/Input"; export { MultiOptionInput } from "./inputs/MultiOptionInput"; - -export { InputFieldWithSelect } from "./inputs/InputFieldWithSelect"; export type { InputFieldProps, InputProps } from "./inputs/types"; export { InputField, Input, TextField, inputStyles } from "./inputs/TextField"; export { InputError } from "./inputs/InputError"; @@ -25,12 +19,9 @@ export { Label } from "./inputs/Label"; export { Select, SelectField, SelectWithValidation, getReactSelectProps } from "./select"; export { DateRangePickerLazy as DateRangePicker } from "./date-range-picker"; -export { Slider } from "./slider"; -export { RangeSlider } from "./slider/RangeSlider"; -export { RangeSliderPopover } from "./slider/RangeSliderPopover"; export { BooleanToggleGroup, BooleanToggleGroupField, ToggleGroup } from "./toggleGroup"; export { DatePicker } from "./datepicker"; -export { FormStep, Steps, Stepper } from "./step"; +export { Steps } from "./step"; export { WizardForm } from "./wizard"; export { default as ColorPicker } from "./color-picker/colorpicker"; export { SettingsToggle, Switch } from "./switch"; diff --git a/packages/ui/components/form/inputs/Input.tsx b/packages/ui/components/form/inputs/Input.tsx index 6f217e25dd1765..88a5bbd734a723 100644 --- a/packages/ui/components/form/inputs/Input.tsx +++ b/packages/ui/components/form/inputs/Input.tsx @@ -14,15 +14,6 @@ import { Input, InputField, inputStyles } from "../inputs/TextField"; import { Label } from "./Label"; import type { InputFieldProps } from "./types"; -export function InputLeading(props: JSX.IntrinsicElements["div"]) { - return ( - - {props.children} - - ); -} - - export const PasswordField = forwardRef(function PasswordField( props, ref @@ -149,24 +140,6 @@ export const TextAreaField = forwardRef ); }); -export function FieldsetLegend(props: JSX.IntrinsicElements["legend"]) { - return ( - - {props.children} - - ); -} - -export function InputGroupBox(props: JSX.IntrinsicElements["div"]) { - return ( -
- {props.children} -
- ); -} - export const NumberInput = forwardRef(function NumberInput(props, ref) { return ( (function EmailField(props, ref) { - return ( - } - /> - ); -}); diff --git a/packages/ui/components/form/inputs/input.test.tsx b/packages/ui/components/form/inputs/input.test.tsx index fc0d6e63580b68..4de924b967f3d9 100644 --- a/packages/ui/components/form/inputs/input.test.tsx +++ b/packages/ui/components/form/inputs/input.test.tsx @@ -1,11 +1,8 @@ -/* eslint-disable playwright/missing-playwright-await */ import { TooltipProvider } from "@radix-ui/react-tooltip"; import { render, fireEvent } from "@testing-library/react"; import { vi } from "vitest"; -import type { UnstyledSelect } from "../../address/Select"; import { EmailField, TextAreaField, PasswordField, NumberInput, FilterSearchField } from "./Input"; -import { InputFieldWithSelect } from "./InputFieldWithSelect"; import { InputField } from "./TextField"; const onChangeMock = vi.fn(); @@ -117,31 +114,6 @@ describe("Tests for TextAreaField Component", () => { }); }); -describe("Tests for InputFieldWithSelect Component", () => { - test("Should render correctly with InputField and UnstyledSelect", () => { - const onChangeMock = vi.fn(); - - const selectProps = { - value: null, - onChange: onChangeMock, - name: "testSelect", - options: [ - { value: "Option 1", label: "Option 1" }, - { value: "Option 2", label: "Option 2" }, - { value: "Option 3", label: "Option 3" }, - ], - } as unknown as typeof UnstyledSelect; - - const { getByText } = render(); - - const inputElement = getByText("Select..."); - fireEvent.mouseDown(inputElement); - - const optionElement = getByText("Option 1"); - expect(optionElement).toBeInTheDocument(); - }); -}); - describe("Tests for NumberInput Component", () => { test("Should render correctly with input type number", () => { const { getByRole } = render(); diff --git a/packages/ui/components/form/slider/RangeSlider.tsx b/packages/ui/components/form/slider/RangeSlider.tsx deleted file mode 100644 index cbbfbb2f8280d1..00000000000000 --- a/packages/ui/components/form/slider/RangeSlider.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import * as SliderPrimitive from "@radix-ui/react-slider"; -import * as React from "react"; - -import classNames from "@calcom/ui/classNames"; - -const RangeSlider = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - - - -)); -RangeSlider.displayName = SliderPrimitive.Root.displayName; - -export { RangeSlider }; diff --git a/packages/ui/components/form/slider/RangeSliderPopover.tsx b/packages/ui/components/form/slider/RangeSliderPopover.tsx deleted file mode 100644 index 4f84dbd9e1cb14..00000000000000 --- a/packages/ui/components/form/slider/RangeSliderPopover.tsx +++ /dev/null @@ -1,129 +0,0 @@ -"use client"; - -import * as Popover from "@radix-ui/react-popover"; -import { useState } from "react"; - -import { Badge } from "../../badge/Badge"; -import { Button } from "../../button/Button"; -import { inputStyles, TextField } from "../inputs/TextField"; -import { RangeSlider } from "./RangeSlider"; - -interface RangeSliderPopoverProps { - triggerText: string; - resetBtnText?: string; - applyBtnText?: string; - value: number[]; - onChange: (value: number[]) => void; - min: number; - max: number; - step?: number; - badgeVariant?: "default" | "success" | "gray" | "warning" | "orange" | "red"; - badgeSuffix?: string; - inputSuffix?: string; - inputLeading?: string; -} - -export const RangeSliderPopover = ({ - resetBtnText = "Reset", - applyBtnText = "Apply", - triggerText, - value, - onChange, - min, - max, - step = 1, - badgeVariant = "default", - badgeSuffix, - inputSuffix, - inputLeading, -}: RangeSliderPopoverProps) => { - const [internalValue, setInternalValue] = useState(value); - const [open, setOpen] = useState(false); - - const handleReset = () => { - setInternalValue([min, max]); - }; - - const handleApply = () => { - onChange(internalValue); - setOpen(false); - }; - - return ( - - - - - - -
- -
-
- { - const newValue = parseInt(e.target.value); - if (!isNaN(newValue) && newValue >= min && newValue <= internalValue[1]) { - setInternalValue([newValue, internalValue[1]]); - } - }} - addOnLeading={inputLeading ? inputLeading : undefined} - addOnSuffix={inputSuffix ? inputSuffix : undefined} - containerClassName="w-full" - /> - { - const newValue = parseInt(e.target.value); - if (!isNaN(newValue) && newValue >= internalValue[0] && newValue <= max) { - setInternalValue([internalValue[0], newValue]); - } - }} - addOnLeading={inputLeading ? inputLeading : undefined} - addOnSuffix={inputSuffix ? inputSuffix : undefined} - containerClassName="w-full" - /> -
- -
-
-
- - -
-
-
-
-
- ); -}; diff --git a/packages/ui/components/form/slider/index.tsx b/packages/ui/components/form/slider/index.tsx deleted file mode 100644 index 254ca364afa1cd..00000000000000 --- a/packages/ui/components/form/slider/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import * as SliderPrimitive from "@radix-ui/react-slider"; -import * as React from "react"; - -import classNames from "@calcom/ui/classNames"; - -const Slider = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - - - -)); -Slider.displayName = SliderPrimitive.Root.displayName; - -export { Slider }; diff --git a/packages/ui/components/form/step/FormStep.tsx b/packages/ui/components/form/step/FormStep.tsx deleted file mode 100644 index b0fec7ba08815c..00000000000000 --- a/packages/ui/components/form/step/FormStep.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react"; - -import classNames from "@calcom/ui/classNames"; - -type Props = { - steps: number; - currentStep: number; -}; - -// It might be worth passing this label string from outside the component so we can translate it? -function FormStep({ currentStep, steps }: Props) { - return ( -
-

- Step {currentStep} of {steps} -

-
- {[...Array(steps)].map((_, j) => { - return ( -
= j ? "bg-black" : "bg-gray-400" - )} - key={j} - /> - ); - })} -
-
- ); -} - -export default FormStep; diff --git a/packages/ui/components/form/step/Stepper.tsx b/packages/ui/components/form/step/Stepper.tsx deleted file mode 100644 index fa75f4011268cd..00000000000000 --- a/packages/ui/components/form/step/Stepper.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useAutoAnimate } from "@formkit/auto-animate/react"; -import Link from "next/link"; - -type DefaultStep = { - title: string; -}; - -function Stepper(props: { - href: string; - step: number; - steps: T[]; - disableSteps?: boolean; - stepLabel?: (currentStep: number, totalSteps: number) => string; -}) { - const { - href, - steps, - stepLabel = (currentStep, totalSteps) => `Step ${currentStep} of ${totalSteps}`, - } = props; - const [stepperRef] = useAutoAnimate(); - return ( - <> - {steps.length > 1 && ( - - )} - - ); -} - -export default Stepper; diff --git a/packages/ui/components/form/step/index.ts b/packages/ui/components/form/step/index.ts index 14316fa07eacfb..bcd0c54fa1d966 100644 --- a/packages/ui/components/form/step/index.ts +++ b/packages/ui/components/form/step/index.ts @@ -1,3 +1 @@ -export { default as FormStep } from "./FormStep"; export { Steps } from "./Steps"; -export { default as Stepper } from "./Stepper"; diff --git a/packages/ui/components/navigation/NavigationItem.tsx b/packages/ui/components/navigation/NavigationItem.tsx deleted file mode 100644 index c6c96832b5e8a2..00000000000000 --- a/packages/ui/components/navigation/NavigationItem.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { cva } from "class-variance-authority"; -import { Fragment } from "react"; - -import classNames from "@calcom/ui/classNames"; - -import { ButtonOrLink } from "../dropdown"; -import { Icon } from "../icon"; -import type { IconName } from "../icon"; - -export type NavigationItemType = { - isLastChild?: boolean; - isExpanded?: boolean; - onToggle?: () => void; - name: string; - href?: string; - isLoading?: boolean; - badge?: React.ReactNode; - icon?: IconName; - child?: NavigationItemType[]; - onlyMobile?: boolean; - onlyDesktop?: boolean; - moreOnMobile?: boolean; - isCurrent?: boolean; -}; - -const navigationItemStyles = cva( - "text-default group flex items-center rounded-[10px] p-2 text-sm font-medium transition hover:bg-subtle hover:text-emphasis", - { - variants: { - isChild: { - true: "[&[aria-current='page']]:text-emphasis [&[aria-current='page']]:bg-emphasis hidden h-8 ml-16 lg:flex lg:ml-10 relative before:absolute before:left-[-24px] before:-top-2 before:h-[calc(100%+0.5rem)] before:w-0.5 before:bg-subtle before:content-[''] first:before:rounded-t-full last:before:rounded-b-full", - false: "[&[aria-current='page']]:text-emphasis mt-0.5 text-sm", - }, - hasChild: { - true: "aria-[aria-current='page']:bg-transparent! relative after:absolute after:left-[-24px] after:top-6 after:h-[calc(100%-1.5rem)] after:w-0.5 after:bg-subtle after:content-[''] first:after:rounded-t-full last:after:rounded-b-full", - false: "[&[aria-current='page']]:bg-subtle", - }, - isFirstChild: { - true: "mt-0", - false: "mt-px", - }, - }, - defaultVariants: { - isChild: false, - hasChild: false, - isFirstChild: false, - }, - } -); - -const Label = ({ children }: { children: React.ReactNode }) => { - return {children}; -}; - -const NavigationItemComponent = ({ - item, - isChild, - index, -}: { - item: NavigationItemType; - isChild?: boolean; - index?: number; -}) => { - return ( - - - {item.icon && ( - - {item.child && - item.isExpanded && - item.child.map((childItem, childIndex) => ( - - ))} - - ); -}; - -export const NavigationItem = Object.assign(NavigationItemComponent, { Label }); diff --git a/packages/ui/components/navigation/index.ts b/packages/ui/components/navigation/index.ts index aef29ad145bf34..a304de05d83cb0 100644 --- a/packages/ui/components/navigation/index.ts +++ b/packages/ui/components/navigation/index.ts @@ -5,4 +5,3 @@ export type { NavTabProps } from "./tabs/HorizontalTabs"; export { default as VerticalTabItem } from "./tabs/VerticalTabItem"; export type { VerticalTabItemProps } from "./tabs/VerticalTabItem"; export { default as VerticalTabs } from "./tabs/VerticalTabs"; -export { NavigationItem } from "./NavigationItem";