diff --git a/.env.appStore.example b/.env.appStore.example index 2719175e4a..15f9fa42fb 100644 --- a/.env.appStore.example +++ b/.env.appStore.example @@ -37,6 +37,7 @@ BASECAMP3_USER_AGENT= DAILY_API_KEY= DAILY_SCALE_PLAN='' +DAILY_WEBHOOK_SECRET='' # - GOOGLE CALENDAR/MEET/LOGIN # Needed to enable Google Calendar integration and Login with Google @@ -126,4 +127,12 @@ ZOHOCRM_CLIENT_ID="" ZOHOCRM_CLIENT_SECRET="" +# - REVERT +# Used for the Pipedrive integration (via/ Revert (https://revert.dev)) +# @see https://github.com/calcom/cal.com/#obtaining-revert-api-keys +REVERT_API_KEY= +REVERT_PUBLIC_TOKEN= + +# NOTE: If you're self hosting Revert, update this URL to point to your own instance. +REVERT_API_URL=https://api.revert.dev/ # ********************************************************************************************************* diff --git a/.env.example b/.env.example index c99d2dff3c..3b7cf159e6 100644 --- a/.env.example +++ b/.env.example @@ -133,6 +133,11 @@ NEXT_PUBLIC_SENDGRID_SENDER_NAME= # Used for capturing exceptions and logging messages NEXT_PUBLIC_SENTRY_DSN= +# Formbricks Experience Management Integration +FORMBRICKS_HOST_URL=https://app.formbricks.com +FORMBRICKS_ENVIRONMENT_ID= +FORMBRICKS_FEEDBACK_SURVEY_ID= + # Twilio # Used to send SMS reminders in workflows TWILIO_SID= @@ -196,6 +201,10 @@ EMAIL_SERVER_PORT=1025 # Make sure to run mailhog container manually or with `yarn dx` E2E_TEST_MAILHOG_ENABLED= +# Resend +# Send transactional email using resend +# RESEND_API_KEY= + # ********************************************************************************************************** # Set the following value to true if you wish to enable Team Impersonation @@ -245,6 +254,18 @@ PROJECT_ID_VERCEL= TEAM_ID_VERCEL= # Get it from: https://vercel.com/account/tokens AUTH_BEARER_TOKEN_VERCEL= +# Add the main domain that you want to use for testing vercel domain management for organizations. This is necessary because WEBAPP_URL of local isn't a valid public domain +# Would create org1.example.com for an org with slug org1 +# LOCAL_TESTING_DOMAIN_VERCEL="example.com" + +## Set it to 1 if you use cloudflare to manage your DNS and would like us to manage the DNS for you for organizations +# CLOUDFLARE_DNS=1 +## Get it from: https://dash.cloudflare.com/profile/api-tokens. Select Edit Zone template and choose a zone(your domain) +# AUTH_BEARER_TOKEN_CLOUDFLARE= +## Zone ID can be found in the Overview tab of your domain in Cloudflare +# CLOUDFLARE_ZONE_ID= +## It should usually work with the default value. This is the DNS CNAME record content to point to Vercel domain +# CLOUDFLARE_VERCEL_CNAME=cname.vercel-dns.com # - APPLE CALENDAR # Used for E2E tests on Apple Calendar @@ -256,6 +277,8 @@ E2E_TEST_APPLE_CALENDAR_PASSWORD="" E2E_TEST_CALCOM_QA_EMAIL="qa@example.com" # Replace with your own password E2E_TEST_CALCOM_QA_PASSWORD="password" +E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS= +E2E_TEST_CALCOM_GCAL_KEYS= # - APP CREDENTIAL SYNC *********************************************************************************** # Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations @@ -289,4 +312,22 @@ E2E_TEST_OIDC_USER_PASSWORD= # redirected from the legacy to the future pages AB_TEST_BUCKET_PROBABILITY=50 # whether we redirect to the future/event-types from event-types or not -APP_ROUTER_EVENT_TYPES_ENABLED=1 +APP_ROUTER_EVENT_TYPES_ENABLED=0 +APP_ROUTER_SETTINGS_ADMIN_ENABLED=0 +APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0 +APP_ROUTER_APPS_SLUG_ENABLED=0 +APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0 +# whether we redirect to the future/apps/categories from /apps/categories or not +APP_ROUTER_APPS_CATEGORIES_ENABLED=0 +# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not +APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0 +APP_ROUTER_BOOKINGS_STATUS_ENABLED=0 +APP_ROUTER_WORKFLOWS_ENABLED=0 +APP_ROUTER_SETTINGS_TEAMS_ENABLED=0 +APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0 +APP_ROUTER_APPS_ENABLED=0 +APP_ROUTER_VIDEO_ENABLED=0 +APP_ROUTER_TEAMS_ENABLED=0 + +# disable setry server source maps +SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0b19591ddd..8691d73057 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -19,11 +19,12 @@ Fixes # (issue) -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] Chore (refactoring code, technical debt, workflow improvements) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] This change requires a documentation update +- Bug fix (non-breaking change which fixes an issue) +- Chore (refactoring code, technical debt, workflow improvements) +- New feature (non-breaking change which adds functionality) +- Breaking change (fix or feature that would cause existing functionality to not work as expected) +- Tests (Unit/Integration/E2E or any other test) +- This change requires a documentation update ## How should this be tested? diff --git a/.github/actions/cache-build/action.yml b/.github/actions/cache-build/action.yml index 1ef35a7831..62bf25dd59 100644 --- a/.github/actions/cache-build/action.yml +++ b/.github/actions/cache-build/action.yml @@ -24,6 +24,8 @@ runs: **/.turbo/** **/dist/** key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }} - - run: yarn build + - run: | + export NODE_OPTIONS="--max_old_space_size=8192" + yarn build if: steps.cache-build.outputs.cache-hit != 'true' shell: bash diff --git a/.github/actions/cache-db/action.yml b/.github/actions/cache-db/action.yml index f69fd9513a..ffc8f15097 100644 --- a/.github/actions/cache-db/action.yml +++ b/.github/actions/cache-db/action.yml @@ -17,10 +17,14 @@ runs: cache-name: cache-db key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }} key-2: ${{ github.event.pull_request.number || github.ref }} + DATABASE_URL: ${{ inputs.DATABASE_URL }} + E2E_TEST_CALCOM_QA_EMAIL: ${{ inputs.E2E_TEST_CALCOM_QA_EMAIL }} + E2E_TEST_CALCOM_QA_PASSWORD: ${{ inputs.E2E_TEST_CALCOM_QA_PASSWORD }} + E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ inputs.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} with: path: ${{ inputs.path }} key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }} - - run: yarn db-seed + - run: echo ${{ env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} && yarn db-seed if: steps.cache-db.outputs.cache-hit != 'true' shell: bash - name: Postgres Dump Backup diff --git a/.github/actions/yarn-playwright-install/action.yml b/.github/actions/yarn-playwright-install/action.yml index 1e118e3099..601513ee76 100644 --- a/.github/actions/yarn-playwright-install/action.yml +++ b/.github/actions/yarn-playwright-install/action.yml @@ -5,7 +5,7 @@ runs: steps: - name: Cache playwright binaries id: playwright-cache - uses: buildjet/cache@v2 + uses: buildjet/cache@v3 with: path: | ~/Library/Caches/ms-playwright diff --git a/.github/workflows/apply-issue-labels-to-pr.yml b/.github/workflows/apply-issue-labels-to-pr.yml deleted file mode 100644 index 3299b591a8..0000000000 --- a/.github/workflows/apply-issue-labels-to-pr.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: "Apply issue labels to PR" - -on: - pull_request_target: - types: - - opened - -jobs: - label_on_pr: - runs-on: ubuntu-latest - - permissions: - contents: none - issues: read - pull-requests: write - - steps: - - name: Apply labels from linked issue to PR - uses: actions/github-script@v5 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - async function getLinkedIssues(owner, repo, prNumber) { - const query = `query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $prNumber) { - closingIssuesReferences(first: 10) { - nodes { - number - labels(first: 10) { - nodes { - name - } - } - } - } - } - } - }`; - - const variables = { - owner: owner, - repo: repo, - prNumber: prNumber, - }; - - const result = await github.graphql(query, variables); - return result.repository.pullRequest.closingIssuesReferences.nodes; - } - - const pr = context.payload.pull_request; - const linkedIssues = await getLinkedIssues( - context.repo.owner, - context.repo.repo, - pr.number - ); - - const labelsToAdd = new Set(); - for (const issue of linkedIssues) { - if (issue.labels && issue.labels.nodes) { - for (const label of issue.labels.nodes) { - labelsToAdd.add(label.name); - } - } - } - - if (labelsToAdd.size) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - labels: Array.from(labelsToAdd), - }); - } diff --git a/.github/workflows/check-types.yml b/.github/workflows/check-types.yml index 644ca2e9d6..85f730a3ae 100644 --- a/.github/workflows/check-types.yml +++ b/.github/workflows/check-types.yml @@ -2,7 +2,7 @@ name: Check types on: workflow_call: env: - NODE_OPTIONS: "--max-old-space-size=8192" + NODE_OPTIONS: --max-old-space-size=4096 jobs: check-types: runs-on: buildjet-4vcpu-ubuntu-2204 diff --git a/.github/workflows/cron-stale-issue.yml b/.github/workflows/cron-stale-issue.yml index 6fdc0d8057..7f66fd0d69 100644 --- a/.github/workflows/cron-stale-issue.yml +++ b/.github/workflows/cron-stale-issue.yml @@ -17,10 +17,11 @@ jobs: steps: - uses: actions/stale@v7 with: + days-before-close: -1 days-before-issue-stale: 60 days-before-issue-close: -1 days-before-pr-stale: 14 - days-before-pr-close: 7 + days-before-pr-close: -1 stale-pr-message: "This PR is being marked as stale due to inactivity." close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued." operations-per-run: 100 diff --git a/.github/workflows/e2e-app-store.yml b/.github/workflows/e2e-app-store.yml index 90d725d96d..28d26f4e40 100644 --- a/.github/workflows/e2e-app-store.yml +++ b/.github/workflows/e2e-app-store.yml @@ -1,7 +1,8 @@ -name: E2E App-Store Apps +name: E2E App-Store Apps Tests on: workflow_call: - +env: + NODE_OPTIONS: --max-old-space-size=4096 jobs: e2e-app-store: timeout-minutes: 20 @@ -29,10 +30,15 @@ jobs: steps: - uses: actions/checkout@v3 - uses: ./.github/actions/dangerous-git-checkout - - run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV - uses: ./.github/actions/yarn-install - uses: ./.github/actions/yarn-playwright-install - uses: ./.github/actions/cache-db + env: + DATABASE_URL: ${{ secrets.CI_DATABASE_URL }} + E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }} + E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }} + E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} + E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }} - uses: ./.github/actions/cache-build - name: Run Tests run: yarn e2e:app-store --shard=${{ matrix.shard }}/${{ strategy.job-total }} @@ -43,6 +49,10 @@ jobs: DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }} E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }} E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }} + E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }} + E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }} + E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} + E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }} E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }} GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }} GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }} @@ -65,7 +75,7 @@ jobs: TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - name: Upload Test Results if: ${{ always() }} - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: app-store-results-${{ matrix.shard }}_${{ strategy.job-total }} path: test-results diff --git a/.github/workflows/e2e-embed-react.yml b/.github/workflows/e2e-embed-react.yml index 5b356246c7..4b3dc67306 100644 --- a/.github/workflows/e2e-embed-react.yml +++ b/.github/workflows/e2e-embed-react.yml @@ -1,7 +1,8 @@ -name: E2E Embed React tests and booking flow(for non-embed as well) +name: E2E Embed React tests and booking flow (for non-embed as well) on: workflow_call: - +env: + NODE_OPTIONS: --max-old-space-size=4096 jobs: e2e-embed: timeout-minutes: 20 @@ -24,7 +25,6 @@ jobs: steps: - uses: actions/checkout@v3 - uses: ./.github/actions/dangerous-git-checkout - - run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV - uses: ./.github/actions/yarn-install - uses: ./.github/actions/yarn-playwright-install - uses: ./.github/actions/cache-db @@ -61,7 +61,7 @@ jobs: TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - name: Upload Test Results if: ${{ always() }} - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: embed-react-results-${{ matrix.shard }}_${{ strategy.job-total }} path: test-results diff --git a/.github/workflows/e2e-embed.yml b/.github/workflows/e2e-embed.yml index bed5801813..e41b1aa545 100644 --- a/.github/workflows/e2e-embed.yml +++ b/.github/workflows/e2e-embed.yml @@ -1,7 +1,8 @@ -name: E2E Embed Core tests and booking flow(for non-embed as well) +name: E2E Embed Core tests and booking flow (for non-embed as well) on: workflow_call: - +env: + NODE_OPTIONS: --max-old-space-size=4096 jobs: e2e-embed: timeout-minutes: 20 @@ -29,7 +30,6 @@ jobs: steps: - uses: actions/checkout@v3 - uses: ./.github/actions/dangerous-git-checkout - - run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV - uses: ./.github/actions/yarn-install - uses: ./.github/actions/yarn-playwright-install - uses: ./.github/actions/cache-db @@ -65,7 +65,7 @@ jobs: TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - name: Upload Test Results if: ${{ always() }} - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: embed-core-results-${{ matrix.shard }}_${{ strategy.job-total }} path: test-results diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 0c084aea3c..4b3e0d94dc 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,8 +1,8 @@ -name: E2E test - +name: E2E tests on: workflow_call: - +env: + NODE_OPTIONS: --max-old-space-size=4096 jobs: e2e: timeout-minutes: 20 @@ -28,7 +28,6 @@ jobs: steps: - uses: actions/checkout@v3 - uses: ./.github/actions/dangerous-git-checkout - - run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV - uses: ./.github/actions/yarn-install - uses: ./.github/actions/yarn-playwright-install - uses: ./.github/actions/cache-db @@ -68,7 +67,7 @@ jobs: TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - name: Upload Test Results if: ${{ always() }} - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: test-results-${{ matrix.shard }}_${{ strategy.job-total }} path: test-results diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 8c5f9c1829..e9074b895e 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,6 +1,7 @@ name: "Pull Request Labeler" on: - - pull_request_target + pull_request_target: + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true @@ -16,3 +17,81 @@ jobs: repo-token: "${{ secrets.GITHUB_TOKEN }}" # https://github.com/actions/labeler/issues/442#issuecomment-1297359481 sync-labels: "" + team-labels: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: equitybee/team-label-action@main + with: + repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }} + organization-name: calcom + ignore-labels: "app-store, ai, authentication, automated-testing, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier" + apply-labels-from-issue: + runs-on: ubuntu-latest + + permissions: + contents: none + issues: read + pull-requests: write + + steps: + - name: Apply labels from linked issue to PR + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + async function getLinkedIssues(owner, repo, prNumber) { + const query = `query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $prNumber) { + closingIssuesReferences(first: 10) { + nodes { + number + labels(first: 10) { + nodes { + name + } + } + } + } + } + } + }`; + + const variables = { + owner: owner, + repo: repo, + prNumber: prNumber, + }; + + const result = await github.graphql(query, variables); + return result.repository.pullRequest.closingIssuesReferences.nodes; + } + + const pr = context.payload.pull_request; + const linkedIssues = await getLinkedIssues( + context.repo.owner, + context.repo.repo, + pr.number + ); + + const labelsToAdd = new Set(); + for (const issue of linkedIssues) { + if (issue.labels && issue.labels.nodes) { + for (const label of issue.labels.nodes) { + labelsToAdd.add(label.name); + } + } + } + + if (labelsToAdd.size) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: Array.from(labelsToAdd), + }); + } diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 47a5c19102..9bb6f3bdf7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,7 +25,7 @@ jobs: - name: Upload ESLint report if: ${{ always() }} - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: lint-results path: lint-results diff --git a/.github/workflows/nextjs-bundle-analysis.yml b/.github/workflows/nextjs-bundle-analysis.yml index 972e8af225..4c8da86217 100644 --- a/.github/workflows/nextjs-bundle-analysis.yml +++ b/.github/workflows/nextjs-bundle-analysis.yml @@ -2,6 +2,7 @@ name: "Next.js Bundle Analysis" on: workflow_call: + workflow_dispatch: push: branches: - main @@ -27,14 +28,14 @@ jobs: npx -p nextjs-bundle-analysis@0.5.0 report - name: Upload bundle - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: bundle path: apps/web/.next/analyze/__bundle_analysis.json - name: Download base branch bundle stats uses: dawidd6/action-download-artifact@v2 - if: success() && github.event.number + if: success() with: workflow: nextjs-bundle-analysis.yml branch: ${{ github.event.pull_request.base.ref }} @@ -54,39 +55,39 @@ jobs: # Either of these arguments can be changed or removed by editing the `nextBundleAnalysis` # entry in your package.json file. - name: Compare with base branch bundle - if: success() && github.event.number + if: success() run: | cd apps/web ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare - name: Get comment body id: get-comment-body - if: success() && github.event.number + if: success() run: | cd apps/web body=$(cat .next/analyze/__bundle_analysis_comment.txt) body="${body//'%'/'%25'}" body="${body//$'\n'/'%0A'}" body="${body//$'\r'/'%0D'}" - echo ::set-output name=body::$body + echo "{name}={$body}" >> $GITHUB_OUTPUT - name: Find Comment - uses: peter-evans/find-comment@v1 - if: success() && github.event.number + uses: peter-evans/find-comment@v2 + if: success() id: fc with: issue-number: ${{ github.event.number }} body-includes: "" - name: Create Comment - uses: peter-evans/create-or-update-comment@v1.4.4 + uses: peter-evans/create-or-update-comment@v3 if: success() && github.event.number && steps.fc.outputs.comment-id == 0 with: issue-number: ${{ github.event.number }} body: ${{ steps.get-comment-body.outputs.body }} - name: Update Comment - uses: peter-evans/create-or-update-comment@v1.4.4 + uses: peter-evans/create-or-update-comment@v3 if: success() && github.event.number && steps.fc.outputs.comment-id != 0 with: issue-number: ${{ github.event.number }} diff --git a/.github/workflows/pr-assign-team-label.yml b/.github/workflows/pr-assign-team-label.yml deleted file mode 100644 index ecb601f75c..0000000000 --- a/.github/workflows/pr-assign-team-label.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Assign PR team labels -on: - pull_request: - branches: - - main - -jobs: - team-labels: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: equitybee/team-label-action@main - with: - repo-token: ${{ secrets.GH_ACCESS_TOKEN }} - organization-name: calcom - ignore-labels: "app-store, ai, authentication, automated-testing, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier" diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index c792d008bf..69d08f19d0 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -4,9 +4,6 @@ on: pull_request_target: branches: - main - paths-ignore: - - "**.md" - - ".github/CODEOWNERS" merge_group: workflow_dispatch: @@ -21,9 +18,7 @@ jobs: permissions: pull-requests: read outputs: - app-store: ${{ steps.filter.outputs.app-store }} - embed: ${{ steps.filter.outputs.embed }} - embed-react: ${{ steps.filter.outputs.embed-react }} + has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }} steps: - uses: actions/checkout@v3 - uses: ./.github/actions/dangerous-git-checkout @@ -31,78 +26,83 @@ jobs: id: filter with: filters: | - app-store: - - 'apps/web/**' - - 'packages/app-store/**' - - 'playwright.config.ts' - embed: - - 'apps/web/**' - - 'packages/embeds/**' - - 'playwright.config.ts' - embed-react: - - 'apps/web/**' - - 'packages/embeds/**' - - 'playwright.config.ts' - + has-files-requiring-all-checks: + - "!(**.md|.github/CODEOWNERS)" type-check: name: Type check + needs: [changes] + if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }} uses: ./.github/workflows/check-types.yml secrets: inherit test: name: Unit tests - uses: ./.github/workflows/test.yml + needs: [changes] + if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }} + uses: ./.github/workflows/unit-tests.yml secrets: inherit lint: name: Linters + needs: [changes] + if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }} uses: ./.github/workflows/lint.yml secrets: inherit build: name: Production build + needs: [changes] + if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }} uses: ./.github/workflows/production-build.yml secrets: inherit build-without-database: name: Production build (without database) + needs: [changes] + if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }} uses: ./.github/workflows/production-build-without-database.yml secrets: inherit e2e: name: E2E tests needs: [changes, lint, build] + if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }} uses: ./.github/workflows/e2e.yml secrets: inherit e2e-app-store: name: E2E App Store tests needs: [changes, lint, build] + if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }} uses: ./.github/workflows/e2e-app-store.yml secrets: inherit e2e-embed: name: E2E embeds tests needs: [changes, lint, build] + if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }} uses: ./.github/workflows/e2e-embed.yml secrets: inherit e2e-embed-react: name: E2E React embeds tests needs: [changes, lint, build] + if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }} uses: ./.github/workflows/e2e-embed-react.yml secrets: inherit analyze: - needs: build + name: Analyze Build + needs: [changes, build] + if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }} uses: ./.github/workflows/nextjs-bundle-analysis.yml secrets: inherit required: - needs: [lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store] + needs: [changes, lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store] if: always() runs-on: buildjet-4vcpu-ubuntu-2204 steps: - name: fail if conditional jobs failed - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled') + if: needs.changes.outputs.has-files-requiring-all-checks == 'true' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')) run: exit 1 diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml new file mode 100644 index 0000000000..1dc28a549a --- /dev/null +++ b/.github/workflows/pre-release.yml @@ -0,0 +1,82 @@ +name: Pre-release checks + +on: + workflow_dispatch: + +jobs: + changes: + name: Detect changes + runs-on: buildjet-4vcpu-ubuntu-2204 + permissions: + pull-requests: read + outputs: + app-store: ${{ steps.filter.outputs.app-store }} + embed: ${{ steps.filter.outputs.embed }} + embed-react: ${{ steps.filter.outputs.embed-react }} + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/dangerous-git-checkout + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + app-store: + - 'apps/web/**' + - 'packages/app-store/**' + - 'playwright.config.ts' + embed: + - 'apps/web/**' + - 'packages/embeds/**' + - 'playwright.config.ts' + embed-react: + - 'apps/web/**' + - 'packages/embeds/**' + - 'playwright.config.ts' + + lint: + name: Linters + uses: ./.github/workflows/lint.yml + secrets: inherit + + build: + name: Production build + uses: ./.github/workflows/production-build.yml + secrets: inherit + + e2e: + name: E2E tests + needs: [changes, lint, build] + uses: ./.github/workflows/e2e.yml + secrets: inherit + + e2e-app-store: + name: E2E App Store tests + needs: [changes, lint, build] + uses: ./.github/workflows/e2e-app-store.yml + secrets: inherit + + e2e-embed: + name: E2E embeds tests + needs: [changes, lint, build] + uses: ./.github/workflows/e2e-embed.yml + secrets: inherit + + e2e-embed-react: + name: E2E React embeds tests + needs: [changes, lint, build] + uses: ./.github/workflows/e2e-embed-react.yml + secrets: inherit + + build-without-database: + name: Production build (without database) + uses: ./.github/workflows/production-build-without-database.yml + secrets: inherit + + required: + needs: [e2e, e2e-app-store, e2e-embed, e2e-embed-react, build-without-database] + if: always() + runs-on: buildjet-4vcpu-ubuntu-2204 + steps: + - name: fail if conditional jobs failed + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled') + run: exit 1 diff --git a/.github/workflows/production-build.yml b/.github/workflows/production-build.yml index 3ea85d6141..4cbddfc0a5 100644 --- a/.github/workflows/production-build.yml +++ b/.github/workflows/production-build.yml @@ -9,6 +9,10 @@ env: DATABASE_URL: ${{ secrets.CI_DATABASE_URL }} E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }} E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }} + E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }} + E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }} + E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} + E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }} GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }} GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }} NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }} diff --git a/.github/workflows/semantic-pull-requests.yml b/.github/workflows/semantic-pull-requests.yml index fcabef9abc..6a06056cbc 100644 --- a/.github/workflows/semantic-pull-requests.yml +++ b/.github/workflows/semantic-pull-requests.yml @@ -44,5 +44,5 @@ jobs: with: header: pr-title-lint-error message: | - Thank you for following the naming conventions! 🙏 Feel free to join our [discord](https://go.cal.com/discord) and post your PR link to [collect XP and win prizes!](https://cal.com/blog/community-incentives) + Thank you for following the naming conventions! 🙏 Feel free to join our [discord](https://go.cal.com/discord) and post your PR link. diff --git a/.github/workflows/test.yml b/.github/workflows/unit-tests.yml similarity index 89% rename from .github/workflows/test.yml rename to .github/workflows/unit-tests.yml index d8ca18d282..14027bf444 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/unit-tests.yml @@ -11,7 +11,6 @@ jobs: steps: - uses: actions/checkout@v3 - uses: ./.github/actions/dangerous-git-checkout - - run: echo 'NODE_OPTIONS="--max_old_space_size=6144"' >> $GITHUB_ENV - uses: ./.github/actions/yarn-install # Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners - run: yarn test diff --git a/.vscode/settings.json b/.vscode/settings.json index 4c07cd934a..c344645fab 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "typescript.tsdk": "node_modules/typescript/lib", "editor.formatOnSave": false, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "typescript.preferences.importModuleSpecifier": "non-relative", "spellright.language": ["en"], diff --git a/.yarn/patches/libphonenumber-js-npm-1.10.12-51c84f8bf1.patch b/.yarn/patches/libphonenumber-js-npm-1.10.51-4ff79b15f8.patch similarity index 86% rename from .yarn/patches/libphonenumber-js-npm-1.10.12-51c84f8bf1.patch rename to .yarn/patches/libphonenumber-js-npm-1.10.51-4ff79b15f8.patch index 7cfa242404..6b0bfb2905 100644 --- a/.yarn/patches/libphonenumber-js-npm-1.10.12-51c84f8bf1.patch +++ b/.yarn/patches/libphonenumber-js-npm-1.10.51-4ff79b15f8.patch @@ -1,15 +1,15 @@ diff --git a/index.cjs b/index.cjs -index b645707a3549fc298508726e404243499bbed499..f34b0891e99b275a9218e253f303f43d31ef3f73 100644 +index c83f700ae9998cd87b4c2d66ecbb2ad3d7b4603c..76a2200b57f0b9243e2c61464d578b67746ad5a4 100644 --- a/index.cjs +++ b/index.cjs @@ -13,8 +13,8 @@ function withMetadataArgument(func, _arguments) { - // https://github.com/babel/babel/issues/2212#issuecomment-131827986 - // An alternative approach: - // https://www.npmjs.com/package/babel-plugin-add-module-exports --exports = module.exports = min.parsePhoneNumberFromString --exports['default'] = min.parsePhoneNumberFromString -+// exports = module.exports = min.parsePhoneNumberFromString -+// exports['default'] = min.parsePhoneNumberFromString - - // `parsePhoneNumberFromString()` named export is now considered legacy: - // it has been promoted to a default export due to being too verbose. + // https://github.com/babel/babel/issues/2212#issuecomment-131827986 + // An alternative approach: + // https://www.npmjs.com/package/babel-plugin-add-module-exports +-exports = module.exports = min.parsePhoneNumberFromString +-exports['default'] = min.parsePhoneNumberFromString ++// exports = module.exports = min.parsePhoneNumberFromString ++// exports['default'] = min.parsePhoneNumberFromString + + // `parsePhoneNumberFromString()` named export is now considered legacy: + // it has been promoted to a default export due to being too verbose. diff --git a/README.md b/README.md index a617a65b1f..2092be150a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@
- The open-source Calendly alternative.
+ The open-source Calendly successor.
Learn more »
@@ -50,7 +50,7 @@
# Scheduling infrastructure for absolutely everyone
-The open source Calendly alternative. You are in charge
+The open source Calendly successor. You are in charge
of your own data, workflow, and appearance.
Calendly and other scheduling tools are awesome. It made our lives massively easier. We're using it for business meetings, seminars, yoga classes, and even calls with our families. However, most tools are very limited in terms of control and customization.
@@ -147,7 +147,7 @@ Here is what you need to be able to run Cal.com.
- Duplicate `.env.example` to `.env`
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
- - Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
+ - Use `openssl rand -base64 32` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
5. Setup Node
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
@@ -216,12 +216,11 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
If you don't want to create a local DB. Then you can also consider using services like railway.app or render.
- - [Setup postgres DB with railway.app](https://arctype.com/postgres/setup/railway-postgres)
+ - [Setup postgres DB with railway.app](https://docs.railway.app/guides/postgresql)
- [Setup postgres DB with render](https://render.com/docs/databases)
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
-1. Set a 24 character random string in your `.env` file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
In a development environment, run:
@@ -555,6 +554,10 @@ following
[Follow these steps](./packages/app-store/zoho-bigin/)
+### Obtaining Pipedrive Client ID and Secret
+
+[Follow these steps](./packages/app-store/pipedrive-crm/)
+
## Workflows
### Setting up SendGrid for Email reminders
diff --git a/__checks__/organization.spec.ts b/__checks__/organization.spec.ts
index 685f97e281..4c6a047341 100644
--- a/__checks__/organization.spec.ts
+++ b/__checks__/organization.spec.ts
@@ -41,6 +41,28 @@ test.describe("Org", () => {
await expectPageToBeServerSideRendered(page);
});
});
+ test.describe("Dynamic Group Booking", () => {
+ test("Dynamic Group booking link should load", async ({ page }) => {
+ const users = [
+ {
+ username: "peer",
+ name: "Peer Richelsen",
+ },
+ {
+ username: "bailey",
+ name: "Bailey Pumfleet",
+ },
+ ];
+ const response = await page.goto(`http://i.cal.com/${users[0].username}+${users[1].username}`);
+ expect(response?.status()).toBe(200);
+ expect(await page.locator('[data-testid="event-title"]').textContent()).toBe("Dynamic");
+
+ expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[0].name);
+ expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[1].name);
+ // 2 users and 1 for the organization(2+1)
+ expect((await page.locator('[data-testid="event-meta"] [data-testid="avatar"]').all()).length).toBe(3);
+ });
+ });
});
// This ensures that the route is actually mapped to a page that is using withEmbedSsr
diff --git a/apps/ai/README.md b/apps/ai/README.md
index fdba31f370..306bf07906 100644
--- a/apps/ai/README.md
+++ b/apps/ai/README.md
@@ -48,17 +48,21 @@ Here is the full architecture:
### Email Router
-To expose the AI app, run `ngrok http 3005` (or the AI app's port number) in a new terminal. You may need to install [nGrok](https://ngrok.com/).
+To expose the AI app, you can use either [Tunnelmole](https://github.com/robbie-cahill/tunnelmole-client), an open source tunnelling tool; or [nGrok](https://ngrok.com/), a popular closed source tunnelling tool.
+
+For Tunnelmole, run `tmole 3005` (or the AI app's port number) in a new terminal. Please replace `3005` with the port number if it is different. In the output, you'll see two URLs, one http and a https (we recommend using the https url for privacy and security). To install Tunnelmole, use `curl -O https://install.tunnelmole.com/8dPBw/install && sudo bash install`. (On Windows, download [tmole.exe](https://tunnelmole.com/downloads/tmole.exe))
+
+For nGrok, run `ngrok http 3005` (or the AI app's port number) in a new terminal. You may need to install nGrok first.
To forward incoming emails to the serverless function at `/agent`, we use [SendGrid's Inbound Parse](https://docs.sendgrid.com/for-developers/parsing-email/setting-up-the-inbound-parse-webhook).
1. Ensure you have a [SendGrid account](https://signup.sendgrid.com/)
2. Ensure you have an authenticated domain. Go to Settings > Sender Authentication > Authenticate. For DNS host, select `I'm not sure`. Click Next and add your domain, eg. `example.com`. Choose Manual Setup. You'll be given three CNAME records to add to your DNS settings, eg. in [Vercel Domains](https://vercel.com/dashboard/domains). After adding those records, click Verify. To troubleshoot, see the [full instructions](https://docs.sendgrid.com/ui/account-and-settings/how-to-set-up-domain-authentication).
-3. Authorize your domain for email with MX records: one with name `[your domain].com` and value `mx.sendgrid.net.`, and another with name `bounces.[your domain].com` and value `feedback-smtp.us-east-1.amazonses.com`, both with priority `10` if prompted.
+3. Authorize your domain for email with MX records: one with name `[your domain].com` and value `mx.sendgrid.net.`, and another with name `bounces.[your domain].com` and value `feedback-smtp.us-east-1.amazonses.com`. Set the priority to `10` if prompted.
4. Go to Settings > [Inbound Parse](https://app.sendgrid.com/settings/parse) > Add Host & URL. Choose your authenticated domain.
-5. In the Destination URL field, use the nGrok URL from above along with the path, `/api/receive`, and one param, `parseKey`, which lives in [this app's .env](/apps/ai/.env.example) under `PARSE_KEY`. The full URL should look like `https://abc.ngrok.io/api/receive?parseKey=ABC-123`.
+5. In the Destination URL field, use the Tunnelmole or ngrok URL from above along with the path, `/api/receive`, and one param, `parseKey`, which lives in [this app's .env](/apps/ai/.env.example) under `PARSE_KEY`. The full URL should look like `https://abc.tunnelmole.net/api/receive?parseKey=ABC-123` or `https://abc.ngrok.io/api/receive?parseKey=ABC-123`.
6. Activate "POST the raw, full MIME message".
-7. Send an email to `[anyUsername]@example.com`. You should see a ping on the nGrok listener and server.
+7. Send an email to `[anyUsername]@example.com`. You should see a ping on the Tunnelmole or ngrok listener and server.
8. Adjust the logic in [receive/route.ts](/apps/ai/src/app/api/receive/route.ts), save to hot-reload, and send another email to test the behaviour.
Please feel free to improve any part of this architecture!
diff --git a/apps/api/lib/validations/event-type.ts b/apps/api/lib/validations/event-type.ts
index 70823213bf..cbabad6dc4 100644
--- a/apps/api/lib/validations/event-type.ts
+++ b/apps/api/lib/validations/event-type.ts
@@ -60,6 +60,7 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
successRedirectUrl: true,
locations: true,
bookingLimits: true,
+ onlyShowFirstAvailableSlot: true,
durationLimits: true,
})
.merge(
@@ -147,6 +148,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
seatsShowAvailabilityCount: true,
bookingFields: true,
bookingLimits: true,
+ onlyShowFirstAvailableSlot: true,
durationLimits: true,
}).merge(
z.object({
diff --git a/apps/api/lib/validations/user.ts b/apps/api/lib/validations/user.ts
index 7c595a3352..98e876c4fe 100644
--- a/apps/api/lib/validations/user.ts
+++ b/apps/api/lib/validations/user.ts
@@ -92,7 +92,7 @@ export const schemaUserBaseBodyParams = User.pick({
// Here we can both require or not (adding optional or nullish) and also rewrite validations for any value
// for example making weekStart only accept weekdays as input
const schemaUserEditParams = z.object({
- email: z.string().email(),
+ email: z.string().email().toLowerCase(),
username: usernameSchema,
weekStart: z.nativeEnum(weekdays).optional(),
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
@@ -114,7 +114,7 @@ const schemaUserEditParams = z.object({
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
const schemaUserCreateParams = z.object({
- email: z.string().email(),
+ email: z.string().email().toLowerCase(),
username: usernameSchema,
weekStart: z.nativeEnum(weekdays).optional(),
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
diff --git a/apps/api/next.config.js b/apps/api/next.config.js
index ba2dcbf89b..4d861ce3c3 100644
--- a/apps/api/next.config.js
+++ b/apps/api/next.config.js
@@ -1,6 +1,8 @@
const { withAxiom } = require("next-axiom");
+const { withSentryConfig } = require("@sentry/nextjs");
-module.exports = withAxiom({
+const plugins = [withAxiom];
+const nextConfig = {
transpilePackages: [
"@calcom/app-store",
"@calcom/core",
@@ -66,4 +68,15 @@ module.exports = withAxiom({
],
};
},
-});
+};
+
+if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
+ nextConfig["sentry"] = {
+ autoInstrumentServerFunctions: true,
+ hideSourceMaps: true,
+ };
+
+ plugins.push(withSentryConfig);
+}
+
+module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
diff --git a/apps/api/pages/api/bookings/[id]/_auth-middleware.ts b/apps/api/pages/api/bookings/[id]/_auth-middleware.ts
index c785ccc3be..0016de42e9 100644
--- a/apps/api/pages/api/bookings/[id]/_auth-middleware.ts
+++ b/apps/api/pages/api/bookings/[id]/_auth-middleware.ts
@@ -6,18 +6,44 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
async function authMiddleware(req: NextApiRequest) {
const { userId, prisma, isAdmin, query } = req;
+ if (isAdmin) {
+ return;
+ }
+
const { id } = schemaQueryIdParseInt.parse(query);
- const userWithBookings = await prisma.user.findUnique({
+ const userWithBookingsAndTeamIds = await prisma.user.findUnique({
where: { id: userId },
- include: { bookings: true },
+ include: {
+ bookings: true,
+ teams: {
+ select: {
+ teamId: true,
+ },
+ },
+ },
});
- if (!userWithBookings) throw new HttpError({ statusCode: 404, message: "User not found" });
+ if (!userWithBookingsAndTeamIds) throw new HttpError({ statusCode: 404, message: "User not found" });
- const userBookingIds = userWithBookings.bookings.map((booking) => booking.id);
+ const userBookingIds = userWithBookingsAndTeamIds.bookings.map((booking) => booking.id);
- if (!isAdmin && !userBookingIds.includes(id)) {
- throw new HttpError({ statusCode: 401, message: "You are not authorized" });
+ if (!userBookingIds.includes(id)) {
+ const teamBookings = await prisma.booking.findUnique({
+ where: {
+ id: id,
+ eventType: {
+ team: {
+ id: {
+ in: userWithBookingsAndTeamIds.teams.map((team) => team.teamId),
+ },
+ },
+ },
+ },
+ });
+
+ if (!teamBookings) {
+ throw new HttpError({ statusCode: 401, message: "You are not authorized" });
+ }
}
}
diff --git a/apps/api/pages/api/event-types/_post.ts b/apps/api/pages/api/event-types/_post.ts
index 20b138f158..6e748a1bc2 100644
--- a/apps/api/pages/api/event-types/_post.ts
+++ b/apps/api/pages/api/event-types/_post.ts
@@ -3,8 +3,10 @@ import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
+import { MembershipRole } from "@calcom/prisma/client";
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
+import { canUserAccessTeamWithRole } from "~/pages/api/teams/[teamId]/_auth-middleware";
import checkParentEventOwnership from "./_utils/checkParentEventOwnership";
import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission";
@@ -316,7 +318,13 @@ async function checkPermissions(req: NextApiRequest) {
statusCode: 401,
message: "ADMIN required for `userId`",
});
- if (!isAdmin && body.teamId)
+ if (
+ body.teamId &&
+ !isAdmin &&
+ !(await canUserAccessTeamWithRole(req.prisma, req.userId, isAdmin, body.teamId, {
+ in: [MembershipRole.OWNER, MembershipRole.ADMIN],
+ }))
+ )
throw new HttpError({
statusCode: 401,
message: "ADMIN required for `teamId`",
diff --git a/apps/api/pages/api/slots/_get.ts b/apps/api/pages/api/slots/_get.ts
index 6393bc79c1..61447e1751 100644
--- a/apps/api/pages/api/slots/_get.ts
+++ b/apps/api/pages/api/slots/_get.ts
@@ -1,5 +1,9 @@
+import timezone from "dayjs/plugin/timezone";
+import utc from "dayjs/plugin/utc";
import type { NextApiRequest, NextApiResponse } from "next";
+import dayjs from "@calcom/dayjs";
+import { isSupportedTimeZone } from "@calcom/lib/date-fns";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { createContext } from "@calcom/trpc/server/createContext";
@@ -9,10 +13,34 @@ import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util
import { TRPCError } from "@trpc/server";
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
+// Apply plugins
+dayjs.extend(utc);
+dayjs.extend(timezone);
+
async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
- const input = getScheduleSchema.parse(req.query);
- return await getAvailableSlots({ ctx: await createContext({ req, res }), input });
+ const { usernameList, ...rest } = req.query;
+ let slugs = usernameList;
+ if (!Array.isArray(usernameList)) {
+ slugs = usernameList ? [usernameList] : [];
+ }
+ const input = getScheduleSchema.parse({ usernameList: slugs, ...rest });
+ const timeZoneSupported = input.timeZone ? isSupportedTimeZone(input.timeZone) : false;
+ const availableSlots = await getAvailableSlots({ ctx: await createContext({ req, res }), input });
+ const slotsInProvidedTimeZone = timeZoneSupported
+ ? Object.keys(availableSlots.slots).reduce(
+ (acc: Record
- Our{" "} - - Figma - {" "} + Our Figma library is available for anyone to view and use. If you have any questions or concerns, please reach out to the design team.
diff --git a/apps/storybook/package.json b/apps/storybook/package.json index 3aae79cb0a..0cc7d6cb30 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -3,8 +3,8 @@ "private": true, "version": "0.0.0", "scripts": { - "dev": "start-storybook -p 6006", - "build": "build-storybook" + "dev": "storybook dev -p 6006", + "build": "storybook build" }, "dependencies": { "@calcom/config": "*", @@ -20,23 +20,25 @@ "@radix-ui/react-slider": "^1.0.0", "@radix-ui/react-switch": "^1.0.0", "@radix-ui/react-tooltip": "^1.0.0", + "@storybook/addon-docs": "^7.6.3", + "@storybook/blocks": "^7.6.3", + "@storybook/nextjs": "^7.6.3", + "@storybook/preview-api": "^7.6.3", "next": "^13.4.6", "react": "^18.2.0", "react-dom": "^18.2.0", - "storybook-addon-next-router": "^4.0.2", "storybook-addon-rtl-direction": "^0.0.19" }, "devDependencies": { "@babel/core": "^7.19.6", - "@storybook/addon-actions": "^6.5.13", - "@storybook/addon-essentials": "^6.5.13", - "@storybook/addon-interactions": "^6.5.13", - "@storybook/addon-links": "^6.5.13", - "@storybook/builder-vite": "^0.2.4", - "@storybook/builder-webpack5": "^6.5.13", - "@storybook/manager-webpack5": "^6.5.13", - "@storybook/react": "^6.5.13", - "@storybook/testing-library": "^0.0.13", + "@storybook/addon-actions": "^7.6.3", + "@storybook/addon-designs": "^7.0.7", + "@storybook/addon-essentials": "^7.6.3", + "@storybook/addon-interactions": "^7.6.3", + "@storybook/addon-links": "^7.6.3", + "@storybook/nextjs": "^7.6.3", + "@storybook/react": "^7.6.3", + "@storybook/testing-library": "^0.2.2", "@types/react": "18.0.26", "@types/react-dom": "^18.0.9", "@vitejs/plugin-react": "^2.2.0", @@ -46,9 +48,8 @@ "postcss": "^8.4.18", "postcss-pseudo-companion-classes": "^0.1.1", "rollup-plugin-polyfill-node": "^0.10.2", - "storybook-addon-designs": "^6.3.1", - "storybook-addon-next": "^1.6.9", - "storybook-react-i18next": "^1.1.2", + "storybook": "^7.6.3", + "storybook-react-i18next": "^2.0.9", "tailwindcss": "^3.3.3", "typescript": "^4.9.4", "vite": "^4.1.2" diff --git a/apps/storybook/styles/storybook-styles.css b/apps/storybook/styles/storybook-styles.css index 8893aebc15..78d8ce13b7 100644 --- a/apps/storybook/styles/storybook-styles.css +++ b/apps/storybook/styles/storybook-styles.css @@ -197,88 +197,91 @@ @layer { :root { /* background */ - - --cal-bg-emphasis: #e5e7eb; - --cal-bg: white; - --cal-bg-subtle: #f3f4f6; - --cal-bg-muted: #f9fafb; - --cal-bg-inverted: #111827; - + + --cal-bg-emphasis: hsla(220,13%,91%,1); + --cal-bg: hsla(0,0%,100%,1); + --cal-bg-subtle: hsla(220, 14%, 96%,1); + --cal-bg-muted: hsla(210,20%,98%,1); + --cal-bg-inverted: hsla(0,0%,6%,1); + /* background -> components*/ - --cal-bg-info: #dee9fc; - --cal-bg-success: #e2fbe8; - --cal-bg-attention: #fceed8; - --cal-bg-error: #f9e3e2; - --cal-bg-dark-error: #752522; - + --cal-bg-info: hsla(218,83%,98%,1); + --cal-bg-success: hsla(134,76%,94%,1); + --cal-bg-attention: hsla(37, 86%, 92%, 1); + --cal-bg-error: hsla(3,66,93,1); + --cal-bg-dark-error: hsla(2, 55%, 30%, 1); + /* Borders */ - --cal-border-emphasis: #9ca3af; - --cal-border: #d1d5db; - --cal-border-subtle: #e5e7eb; - --cal-border-muted: #f3f4f6; - --cal-border-error: #aa2e26; - + --cal-border-emphasis: hsla(218, 11%, 65%, 1); + --cal-border: hsla(216, 12%, 84%, 1); + --cal-border-subtle: hsla(220, 13%, 91%, 1); + --cal-border-booker: #e5e7eb; + --cal-border-muted: hsla(220, 14%, 96%, 1); + --cal-border-error: hsla(4, 63%, 41%, 1); + /* Content/Text */ - --cal-text-emphasis: #111827; - --cal-text: #374151; - --cal-text-subtle: #6b7280; - --cal-text-muted: #9ca3af; - --cal-text-inverted: white; - + --cal-text-emphasis: hsla(217, 19%, 27%, 1); + --cal-text: hsla(217, 19%, 27%, 1); + --cal-text-subtle: hsla(220, 9%, 46%, 1); + --cal-text-muted: hsla(218, 11%, 65%, 1); + --cal-text-inverted: hsla(0, 0%, 100%, 1); + /* Content/Text -> components */ - --cal-text-info: #253985; - --cal-text-success: #285231; - --cal-text-attention: #73321b; - --cal-text-error: #752522; - + --cal-text-info: hsla(228, 56%, 33%, 1); + --cal-text-success: hsla(133, 34%, 24%, 1); + --cal-text-attention: hsla(16, 62%, 28%, 1); + --cal-text-error: hsla(2, 55%, 30%, 1); + /* Brand shinanigans - -> These will be computed for the users theme at runtime. - */ - --cal-brand: #111827; - --cal-brand-emphasis: #101010; - --cal-brand-text: white; + -> These will be computed for the users theme at runtime. + */ + --cal-brand: hsla(221, 39%, 11%, 1); + --cal-brand-emphasis: hsla(0, 0%, 6%, 1); + --cal-brand-text: hsla(0, 0%, 100%, 1); } .dark { /* background */ - - --cal-bg-emphasis: #2b2b2b; - --cal-bg: #101010; - --cal-bg-subtle: #2b2b2b; - --cal-bg-muted: #1c1c1c; - --cal-bg-inverted: #f3f4f6; - + + --cal-bg-emphasis: hsla(0, 0%, 32%, 1); + --cal-bg: hsla(0, 0%, 10%, 1); + --cal-bg-subtle: hsla(0, 0%, 18%, 1); + --cal-bg-muted: hsla(0, 0%, 12%, 1); + --cal-bg-inverted: hsla(220, 14%, 96%, 1); + /* background -> components*/ - --cal-bg-info: #263fa9; - --cal-bg-success: #306339; - --cal-bg-attention: #8e3b1f; - --cal-bg-error: #8c2822; - --cal-bg-dark-error: #752522; - + --cal-bg-info: hsla(228, 56%, 33%, 1); + --cal-bg-success: hsla(133, 34%, 24%, 1); + --cal-bg-attention: hsla(16, 62%, 28%, 1); + --cal-bg-error: hsla(2, 55%, 30%, 1); + --cal-bg-dark-error: hsla(2, 55%, 30%, 1); + /* Borders */ - --cal-border-emphasis: #575757; - --cal-border: #444444; - --cal-border-subtle: #2b2b2b; - --cal-border-muted: #1c1c1c; - --cal-border-error: #aa2e26; - + --cal-border-emphasis: hsla(0, 0%, 46%, 1); + --cal-border: hsla(0, 0%, 34%, 1); + --cal-border-subtle: hsla(0, 0%, 22%, 1); + --cal-border-booker: hsla(0, 0%, 22%, 1); + --cal-border-muted: hsla(0, 0%, 18%, 1); + --cal-border-error: hsla(4, 63%, 41%, 1); + /* Content/Text */ - --cal-text-emphasis: #f3f4f6; - --cal-text: #d6d6d6; - --cal-text-subtle: #757575; - --cal-text-muted: #575757; - --cal-text-inverted: #101010; - + --cal-text-emphasis: hsla(240, 20%, 99%, 1); + --cal-text: hsla(0, 0%, 84%, 1); + --cal-text-subtle: hsla(0, 0%, 65%, 1); + --cal-text-muted: hsla(0, 0%, 34%, 1); + --cal-text-inverted: hsla(0, 0%, 10%, 1); + /* Content/Text -> components */ - --cal-text-info: #dee9fc; - --cal-text-success: #e2fbe8; - --cal-text-attention: #fceed8; - --cal-text-error: #f9e3e2; - + --cal-text-info: hsla(218, 83%, 93%, 1); + --cal-text-success: hsla(134, 76%, 94%, 1); + --cal-text-attention: hsla(37, 86%, 92%, 1); + --cal-text-error: hsla(3, 66%, 93%, 1); + /* Brand shenanigans - -> These will be computed for the users theme at runtime. - */ - --cal-brand: white; - --cal-brand-emphasis: #e1e1e1; - --cal-brand-text: black; + -> These will be computed for the users theme at runtime. + */ + --cal-brand: hsla(0, 0%, 100%, 1); + --cal-brand-emphasis: hsla(218, 11%, 65%, 1); + --cal-brand-text: hsla(0, 0%, 0%,1); } + } diff --git a/apps/web/abTest/middlewareFactory.ts b/apps/web/abTest/middlewareFactory.ts index 5e300cf0d6..ef5c93ebc7 100644 --- a/apps/web/abTest/middlewareFactory.ts +++ b/apps/web/abTest/middlewareFactory.ts @@ -5,6 +5,19 @@ import z from "zod"; const ROUTES: [URLPattern, boolean][] = [ ["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const, + ["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const, + ["/apps/installed/:category", process.env.APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED === "1"] as const, + ["/apps/:slug", process.env.APP_ROUTER_APPS_SLUG_ENABLED === "1"] as const, + ["/apps/:slug/setup", process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED === "1"] as const, + ["/apps/categories", process.env.APP_ROUTER_APPS_CATEGORIES_ENABLED === "1"] as const, + ["/apps/categories/:category", process.env.APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED === "1"] as const, + ["/workflows/:path*", process.env.APP_ROUTER_WORKFLOWS_ENABLED === "1"] as const, + ["/settings/teams/:path*", process.env.APP_ROUTER_SETTINGS_TEAMS_ENABLED === "1"] as const, + ["/getting-started/:step", process.env.APP_ROUTER_GETTING_STARTED_STEP_ENABLED === "1"] as const, + ["/apps", process.env.APP_ROUTER_APPS_ENABLED === "1"] as const, + ["/bookings/:status", process.env.APP_ROUTER_BOOKINGS_STATUS_ENABLED === "1"] as const, + ["/video/:path*", process.env.APP_ROUTER_VIDEO_ENABLED === "1"] as const, + ["/teams", process.env.APP_ROUTER_TEAMS_ENABLED === "1"] as const, ].map(([pathname, enabled]) => [ new URLPattern({ pathname, @@ -27,7 +40,6 @@ export const abTestMiddlewareFactory = const override = req.cookies.has(FUTURE_ROUTES_OVERRIDE_COOKIE_NAME); const route = ROUTES.find(([regExp]) => regExp.test(req.url)) ?? null; - const enabled = route !== null ? route[1] || override : false; if (pathname.includes("future") || !enabled) { diff --git a/apps/web/app/_trpc/serverClient.ts b/apps/web/app/_trpc/serverClient.ts new file mode 100644 index 0000000000..e4b7d577f0 --- /dev/null +++ b/apps/web/app/_trpc/serverClient.ts @@ -0,0 +1,4 @@ +import type { TRPCContext } from "@calcom/trpc/server/createContext"; +import { appRouter } from "@calcom/trpc/server/routers/_app"; + +export const getServerCaller = (ctx: TRPCContext) => appRouter.createCaller(ctx); diff --git a/apps/web/app/_trpc/trpc-provider.tsx b/apps/web/app/_trpc/trpc-provider.tsx index f6d2ed2817..6e81d2996a 100644 --- a/apps/web/app/_trpc/trpc-provider.tsx +++ b/apps/web/app/_trpc/trpc-provider.tsx @@ -8,33 +8,8 @@ import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink"; import { httpLink } from "@calcom/trpc/client/links/httpLink"; import { loggerLink } from "@calcom/trpc/client/links/loggerLink"; import { splitLink } from "@calcom/trpc/client/links/splitLink"; +import { ENDPOINTS } from "@calcom/trpc/react/shared"; -const ENDPOINTS = [ - "admin", - "apiKeys", - "appRoutingForms", - "apps", - "auth", - "availability", - "appBasecamp3", - "bookings", - "deploymentSetup", - "eventTypes", - "features", - "insights", - "payments", - "public", - "saml", - "slots", - "teams", - "organizations", - "users", - "viewer", - "webhook", - "workflows", - "appsRouter", - "googleWorkspace", -] as const; export type Endpoint = (typeof ENDPOINTS)[number]; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/apps/web/app/_types.ts b/apps/web/app/_types.ts new file mode 100644 index 0000000000..0e6ce75a13 --- /dev/null +++ b/apps/web/app/_types.ts @@ -0,0 +1,14 @@ +export type Params = { + [param: string]: string | string[] | undefined; +}; + +export type SearchParams = { + [param: string]: string | string[] | undefined; +}; + +export type PageProps = { + params: Params; + searchParams: SearchParams; +}; + +export type LayoutProps = { params: Params; children: React.ReactElement }; diff --git a/apps/web/app/future/(shared-page-wrapper)/(layout)/layout.tsx b/apps/web/app/future/(shared-page-wrapper)/(layout)/layout.tsx deleted file mode 100644 index 7d78c3b422..0000000000 --- a/apps/web/app/future/(shared-page-wrapper)/(layout)/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -// pages without layout (e.g., /availability/index.tsx) are supposed to go under (layout) folder -import { headers } from "next/headers"; -import { type ReactElement } from "react"; - -import { getLayout } from "@calcom/features/MainLayoutAppDir"; - -import PageWrapper from "@components/PageWrapperAppDir"; - -type WrapperWithLayoutProps = { - children: ReactElement; -}; - -export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) { - const h = headers(); - const nonce = h.get("x-nonce") ?? undefined; - - return ( -(p: P extends "P" ? PageProps : LayoutProps) => {
+ const h = headers();
+ const nonce = h.get("x-nonce") ?? undefined;
+ const props = getData ? await getData(buildLegacyCtx(h, cookies(), p.params)) : ({} as T);
+
+ const children = "children" in p ? p.children : null;
+
+ return (
+
{formatTime(aDate, userTimeFormat, userTimeZone)}
diff --git a/apps/web/components/dialog/EditLocationDialog.tsx b/apps/web/components/dialog/EditLocationDialog.tsx
index 252d5fcf92..4aee471929 100644
--- a/apps/web/components/dialog/EditLocationDialog.tsx
+++ b/apps/web/components/dialog/EditLocationDialog.tsx
@@ -382,7 +382,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
}}
/>
{selectedLocation && LocationOptions}
- {t("warning_payment_instant_meeting_event")}
+ )}
+ {provider?.label
+ ? t("join_event_location", { eventLocationType: provider?.label })
+ : t("join_meeting")}
+