Compare commits
1 Commits
main
...
more-avata
Author | SHA1 | Date | |
---|---|---|---|
a1614767ac |
|
@ -37,7 +37,6 @@ 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
|
||||
|
@ -127,12 +126,4 @@ 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/
|
||||
# *********************************************************************************************************
|
||||
|
|
78
.env.example
78
.env.example
|
@ -25,7 +25,6 @@ CALCOM_LICENSE_KEY=
|
|||
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||
UPSTASH_REDIS_REST_URL=
|
||||
UPSTASH_REDIS_REST_TOKEN=
|
||||
INSIGHTS_DATABASE_URL=
|
||||
|
||||
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||
|
@ -107,19 +106,6 @@ NEXT_PUBLIC_HELPSCOUT_KEY=
|
|||
NEXT_PUBLIC_FRESHCHAT_TOKEN=
|
||||
NEXT_PUBLIC_FRESHCHAT_HOST=
|
||||
|
||||
# Google OAuth credentials
|
||||
# To enable Login with Google you need to:
|
||||
# 1. Set `GOOGLE_API_CREDENTIALS` above
|
||||
# 2. Set `GOOGLE_LOGIN_ENABLED` to `true`
|
||||
# When self-hosting please ensure you configure the Google integration as an Internal app so no one else can login to your instance
|
||||
# @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications
|
||||
GOOGLE_LOGIN_ENABLED=false
|
||||
|
||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||
# Needed to enable Google Calendar integration and Login with Google
|
||||
# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials
|
||||
GOOGLE_API_CREDENTIALS=
|
||||
|
||||
# Inbox to send user feedback
|
||||
SEND_FEEDBACK_EMAIL=
|
||||
|
||||
|
@ -129,15 +115,6 @@ SENDGRID_API_KEY=
|
|||
SENDGRID_EMAIL=
|
||||
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
|
||||
|
||||
# Sentry
|
||||
# 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=
|
||||
|
@ -149,7 +126,7 @@ TWILIO_WHATSAPP_PHONE_NUMBER=
|
|||
NEXT_PUBLIC_SENDER_ID=
|
||||
TWILIO_VERIFY_SID=
|
||||
|
||||
# Set it to "1" if you need to run E2E tests locally.
|
||||
# Set it to "1" if you need to run E2E tests locally.
|
||||
NEXT_PUBLIC_IS_E2E=
|
||||
|
||||
# Used for internal billing system
|
||||
|
@ -160,7 +137,6 @@ NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
|
|||
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||
STRIPE_ORG_MONTHLY_PRICE_ID=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_WEBHOOK_SECRET_APPS=
|
||||
STRIPE_PRIVATE_KEY=
|
||||
STRIPE_CLIENT_ID=
|
||||
PAYMENT_FEE_FIXED=
|
||||
|
@ -201,10 +177,6 @@ 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
|
||||
|
@ -231,8 +203,6 @@ CSP_POLICY=
|
|||
EDGE_CONFIG=
|
||||
|
||||
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||
# Control time intervals on a user's Schedule availability
|
||||
NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL=
|
||||
|
||||
# - ORGANIZATIONS *******************************************************************************************
|
||||
# Enable Organizations non-prod domain setup, works in combination with organizations feature flag
|
||||
|
@ -254,32 +224,12 @@ 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
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL=""
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD=""
|
||||
|
||||
# - CALCOM QA ACCOUNT
|
||||
# Used for E2E tests on Cal.com that require 3rd party integrations
|
||||
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
|
||||
# Under settings/admin/apps ensure that all app secrets are set the same as the parent application
|
||||
|
@ -289,7 +239,7 @@ CALCOM_WEBHOOK_SECRET=""
|
|||
CALCOM_WEBHOOK_HEADER_NAME="calcom-webhook-secret"
|
||||
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
|
||||
# Key should match on Cal.com and your application
|
||||
# must be 24 bytes for AES256 encryption algorithm
|
||||
# must be 32 bytes for AES256 encryption algorithm
|
||||
# You can use: `openssl rand -base64 24` to generate one
|
||||
CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY=""
|
||||
|
||||
|
@ -307,27 +257,3 @@ E2E_TEST_OIDC_USER_EMAIL=
|
|||
E2E_TEST_OIDC_USER_PASSWORD=
|
||||
|
||||
# ***********************************************************************************************************
|
||||
|
||||
# provide a value between 0 and 100 to ensure the percentage of traffic
|
||||
# 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=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
|
||||
|
|
|
@ -47,9 +47,3 @@ assignees: ""
|
|||
-->
|
||||
|
||||
(Share it here.)
|
||||
|
||||
---
|
||||
##### House rules
|
||||
- If this issue has a `🚨 needs approval` label, don't start coding yet. Wait until a core member approves feature request by removing this label, then you can start coding.
|
||||
- For clarity: Non-core member issues automatically get the `🚨 needs approval` label.
|
||||
- Your feature ideas are invaluable to us! However, they undergo review to ensure alignment with the product's direction.
|
||||
|
|
|
@ -19,12 +19,11 @@ Fixes # (issue)
|
|||
|
||||
<!-- Please delete bullets that are not relevant. -->
|
||||
|
||||
- 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
|
||||
- [ ] 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
|
||||
|
||||
## How should this be tested?
|
||||
|
||||
|
@ -41,7 +40,7 @@ Fixes # (issue)
|
|||
|
||||
## Checklist
|
||||
|
||||
<!-- Remove bullet points below that don't apply to you -->
|
||||
<!-- Please remove all the irrelevant bullets to your PR -->
|
||||
|
||||
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
||||
- My code doesn't follow the style guidelines of this project
|
||||
|
|
|
@ -24,8 +24,6 @@ runs:
|
|||
**/.turbo/**
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn build
|
||||
- run: yarn build
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
|
|
|
@ -17,14 +17,10 @@ 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: echo ${{ env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} && yarn db-seed
|
||||
- run: yarn db-seed
|
||||
if: steps.cache-db.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
- name: Postgres Dump Backup
|
||||
|
|
|
@ -5,7 +5,7 @@ runs:
|
|||
steps:
|
||||
- name: Cache playwright binaries
|
||||
id: playwright-cache
|
||||
uses: buildjet/cache@v3
|
||||
uses: buildjet/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/Library/Caches/ms-playwright
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
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),
|
||||
});
|
||||
}
|
|
@ -2,7 +2,7 @@ name: Check types
|
|||
on:
|
||||
workflow_call:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||
jobs:
|
||||
check-types:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
|
|
|
@ -17,11 +17,10 @@ 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: -1
|
||||
days-before-pr-close: 7
|
||||
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
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
name: E2E App-Store Apps Tests
|
||||
name: E2E App-Store Apps
|
||||
on:
|
||||
workflow_call:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
jobs:
|
||||
e2e-app-store:
|
||||
timeout-minutes: 20
|
||||
|
@ -30,15 +29,10 @@ 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 }}
|
||||
|
@ -49,10 +43,6 @@ 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 }}
|
||||
|
@ -75,7 +65,7 @@ jobs:
|
|||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-store-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||
path: test-results
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
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
|
||||
|
@ -25,6 +24,7 @@ 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@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: embed-react-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||
path: test-results
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
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
|
||||
|
@ -30,6 +29,7 @@ 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@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: embed-core-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||
path: test-results
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
name: E2E tests
|
||||
name: E2E test
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
timeout-minutes: 20
|
||||
|
@ -28,6 +28,7 @@ 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
|
||||
|
@ -67,7 +68,7 @@ jobs:
|
|||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: test-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||
path: test-results
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
name: "Pull Request Labeler"
|
||||
on:
|
||||
pull_request_target:
|
||||
workflow_dispatch:
|
||||
- pull_request_target
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
@ -17,81 +16,3 @@ 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: "admin, app-store, ai, authentication, automated-testing, devops, 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),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
|
||||
- name: Upload ESLint report
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: lint-results
|
||||
path: lint-results
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
name: Auto Comment Merge Conflicts
|
||||
on: push
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
auto-comment-merge-conflicts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: codytseng/auto-comment-merge-conflicts@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
comment-body: "Hey there, there is a merge conflict, can you take a look?"
|
||||
wait-ms: 3000
|
||||
max-retries: 5
|
||||
label-name: "🚨 merge conflict"
|
||||
ignore-authors: dependabot,otherAuthor
|
|
@ -2,7 +2,6 @@ name: "Next.js Bundle Analysis"
|
|||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
@ -28,14 +27,14 @@ jobs:
|
|||
npx -p nextjs-bundle-analysis@0.5.0 report
|
||||
|
||||
- name: Upload bundle
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
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()
|
||||
if: success() && github.event.number
|
||||
with:
|
||||
workflow: nextjs-bundle-analysis.yml
|
||||
branch: ${{ github.event.pull_request.base.ref }}
|
||||
|
@ -55,7 +54,7 @@ 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()
|
||||
if: success() && github.event.number
|
||||
run: |
|
||||
cd apps/web
|
||||
ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare
|
||||
|
@ -69,10 +68,10 @@ jobs:
|
|||
body="${body//'%'/'%25'}"
|
||||
body="${body//$'\n'/'%0A'}"
|
||||
body="${body//$'\r'/'%0D'}"
|
||||
echo "{body}=${body}" >> $GITHUB_OUTPUT
|
||||
echo ::set-output name=body::$body
|
||||
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v2
|
||||
uses: peter-evans/find-comment@v1
|
||||
if: success() && github.event.number
|
||||
id: fc
|
||||
with:
|
||||
|
@ -80,14 +79,14 @@ jobs:
|
|||
body-includes: "<!-- __NEXTJS_BUNDLE_@calcom/web -->"
|
||||
|
||||
- name: Create Comment
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||
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@v3
|
||||
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||
if: success() && github.event.number && steps.fc.outputs.comment-id != 0
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
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, 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"
|
|
@ -4,6 +4,9 @@ on:
|
|||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- ".github/CODEOWNERS"
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
|
||||
|
@ -12,21 +15,15 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
login:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
|
||||
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
|
||||
|
@ -34,83 +31,78 @@ jobs:
|
|||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
has-files-requiring-all-checks:
|
||||
- "!(**.md|.github/CODEOWNERS)"
|
||||
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'
|
||||
|
||||
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
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
uses: ./.github/workflows/test.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:
|
||||
name: Analyze Build
|
||||
needs: [changes, build]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: build
|
||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [changes, lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
needs: [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: needs.changes.outputs.has-files-requiring-all-checks == 'true' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled'))
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||
run: exit 1
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
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
|
|
@ -9,10 +9,6 @@ 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 }}
|
||||
|
|
|
@ -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.
|
||||
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)
|
||||
|
||||
|
|
|
@ -11,8 +11,7 @@ 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
|
||||
# We could add different timezones here that we need to run our tests in
|
||||
- run: TZ=America/Los_Angeles yarn test -- --timeZoneDependentTestsOnly
|
|
@ -0,0 +1,27 @@
|
|||
name: "Welcome new contributors"
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: opened
|
||||
pull_request:
|
||||
types: opened
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
welcome-message:
|
||||
name: Welcoming New Users
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr-message: |-
|
||||
Thank you for making your first Pull Request and taking the time to improve Cal.com ! ❤️🎉
|
||||
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)
|
||||
issue-message: |
|
||||
Thank you for opening your first issue, one of our team members will review it as soon as it possible. ❤️🎉
|
|
@ -2,7 +2,7 @@
|
|||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"spellright.language": ["en"],
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
diff --git a/index.cjs b/index.cjs
|
||||
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.
|
|
@ -1,26 +0,0 @@
|
|||
diff --git a/dist/commonjs/serverSideTranslations.js b/dist/commonjs/serverSideTranslations.js
|
||||
index bcad3d02fbdfab8dacb1d85efd79e98623a0c257..fff668f598154a13c4030d1b4a90d5d9c18214ad 100644
|
||||
--- a/dist/commonjs/serverSideTranslations.js
|
||||
+++ b/dist/commonjs/serverSideTranslations.js
|
||||
@@ -36,7 +36,6 @@ var _fs = _interopRequireDefault(require("fs"));
|
||||
var _path = _interopRequireDefault(require("path"));
|
||||
var _createConfig = require("./config/createConfig");
|
||||
var _node = _interopRequireDefault(require("./createClient/node"));
|
||||
-var _appWithTranslation = require("./appWithTranslation");
|
||||
var _utils = require("./utils");
|
||||
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
|
||||
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
|
||||
@@ -110,12 +109,8 @@ var serverSideTranslations = /*#__PURE__*/function () {
|
||||
lng: initialLocale
|
||||
}));
|
||||
localeExtension = config.localeExtension, localePath = config.localePath, fallbackLng = config.fallbackLng, reloadOnPrerender = config.reloadOnPrerender;
|
||||
- if (!reloadOnPrerender) {
|
||||
- _context.next = 18;
|
||||
- break;
|
||||
- }
|
||||
_context.next = 18;
|
||||
- return _appWithTranslation.globalI18n === null || _appWithTranslation.globalI18n === void 0 ? void 0 : _appWithTranslation.globalI18n.reloadResources();
|
||||
+ return void 0;
|
||||
case 18:
|
||||
_createClient = (0, _node["default"])(_objectSpread(_objectSpread({}, config), {}, {
|
||||
lng: initialLocale
|
|
@ -2,18 +2,7 @@
|
|||
|
||||
Contributions are what makes the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
||||
|
||||
## House rules
|
||||
|
||||
- Before submitting a new issue or PR, check if it already exists in [issues](https://github.com/calcom/cal.com/issues) or [PRs](https://github.com/calcom/cal.com/pulls).
|
||||
- GitHub issues: take note of the `🚨 needs approval` label.
|
||||
- **For Contributors**:
|
||||
- Feature Requests: Wait for a core member to approve and remove the `🚨 needs approval` label before you start coding or submit a PR.
|
||||
- Bugs, Security, Performance, Documentation, etc.: You can start coding immediately, even if the `🚨 needs approval` label is present. This label mainly concerns feature requests.
|
||||
- **Our Process**:
|
||||
- Issues from non-core members automatically receive the `🚨 needs approval` label.
|
||||
- We greatly value new feature ideas. To ensure consistency in the product's direction, they undergo review and approval.
|
||||
|
||||
|
||||
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
|
||||
|
||||
## Priorities
|
||||
|
||||
|
|
37
PRIVACY.md
37
PRIVACY.md
|
@ -1,37 +0,0 @@
|
|||
## PRIVACY POLICY
|
||||
Last updated January 28, 2024
|
||||
|
||||
|
||||
### Introduction
|
||||
This is the privacy notice of MaroCalendar, a personal calendar booking service that is only used by me, Gustavo Maronato, to manage my personal and work calendars, and allow you to book events with me. Here, you'll find a description of how and why I might collect, store, and use your information when you book an event with me using this service.
|
||||
|
||||
The service is located at [https://cal.maronato.dev](https://cal.maronato.dev).
|
||||
|
||||
### Questions or concerns?
|
||||
Reading this privacy notice will help you understand your privacy rights and choices. If you do not agree with my policies and practices, please do not access or book an event with me using this service.
|
||||
|
||||
## SUMMARY OF KEY POINTS
|
||||
|
||||
### What personal information do I process?
|
||||
When you choose to book an event with me, you provide me with your name and email address.
|
||||
|
||||
### Do I process any sensitive personal information?
|
||||
I do not process sensitive personal information.
|
||||
|
||||
### Do I receive any information from third parties?
|
||||
I do not receive any information from third parties.
|
||||
|
||||
### How do I process your information?
|
||||
When you book an event with me, I use your name and email address to send you an email with a calendar invite to the event you booked.
|
||||
|
||||
### In what situations and with which parties do I share personal information?
|
||||
The information you submit is used to create a booking between myself and you. I do not share your information with any third parties.
|
||||
|
||||
### How do I keep your information safe?
|
||||
I use reasonable and appropriate security measures to protect your personal information from loss, misuse, and unauthorized access, disclosure, alteration, and destruction.
|
||||
|
||||
### What are your rights?
|
||||
You can cancel your booking at any time by clicking the link in the confirmation email you received when you booked the event.
|
||||
|
||||
### Google Calendar
|
||||
I use a Google Calendar oAuth integration to automatically display to you what are my free time slots and manage the events you book on my calendar. You do not interact with this integration and you are not allowed to use your Google account to add this integration to your Google Calendar. This integration is only used by me to manage my calendar and is not shared with anyone else.
|
17
README.md
17
README.md
|
@ -7,7 +7,7 @@
|
|||
<h3 align="center">Cal.com (formerly Calendso)</h3>
|
||||
|
||||
<p align="center">
|
||||
The open-source Calendly successor.
|
||||
The open-source Calendly alternative.
|
||||
<br />
|
||||
<a href="https://cal.com"><strong>Learn more »</strong></a>
|
||||
<br />
|
||||
|
@ -50,7 +50,7 @@
|
|||
|
||||
# Scheduling infrastructure for absolutely everyone
|
||||
|
||||
The open source Calendly successor. You are in charge
|
||||
The open source Calendly alternative. 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.
|
||||
|
@ -122,7 +122,7 @@ Here is what you need to be able to run Cal.com.
|
|||
|
||||
### Setup
|
||||
|
||||
1. Clone the repo into a public GitHub repository (or fork https://github.com/calcom/cal.com/fork). If you plan to distribute the code, keep the source code public to comply with [AGPLv3](https://github.com/calcom/cal.com/blob/main/LICENSE). To clone in a private repository, [acquire a commercial license](https://cal.com/sales)
|
||||
1. Clone the repo into a public GitHub repository (or fork https://github.com/calcom/cal.com/fork). If you plan to distribute the code, keep the source code public to comply with [AGPLv3](https://github.com/calcom/cal.com/blob/main/LICENSE). To clone in a private repository, [acquire a commercial license](https://cal.com/sales))
|
||||
|
||||
```sh
|
||||
git clone https://github.com/calcom/cal.com.git
|
||||
|
@ -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 32` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||
- Use `openssl rand -base64 24` 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,11 +216,12 @@ 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://docs.railway.app/guides/postgresql)
|
||||
- [Setup postgres DB with railway.app](https://arctype.com/postgres/setup/railway-postgres)
|
||||
- [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 32 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:
|
||||
|
@ -554,10 +555,6 @@ 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
|
||||
|
@ -600,6 +597,8 @@ Distributed under the [AGPLv3 License](https://github.com/calcom/cal.com/blob/ma
|
|||
|
||||
Special thanks to these amazing projects which help power Cal.com:
|
||||
|
||||
[<img src="https://cal.com/powered-by-vercel.svg">](https://vercel.com/?utm_source=calend-so&utm_campaign=oss)
|
||||
|
||||
- [Vercel](https://vercel.com/?utm_source=calend-so&utm_campaign=oss)
|
||||
- [Next.js](https://nextjs.org/)
|
||||
- [Day.js](https://day.js.org/)
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
Terms of Service
|
||||
----------------
|
||||
|
||||
Effective date: 01/28/2024
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
These are the terms of service for my personal calendar booking service, MaroCalendar. You may use this service to book events with me by providing your name, email address, and date/time preferences.
|
||||
|
||||
These Terms of Service (“Terms”, “Terms of Service”) govern your use of this service located at https://cal.maronato.dev operated by Gustavo Maronato.
|
||||
|
||||
You can also find it's privacy policy here https://git.maronato.dev/maronato/cal/src/branch/main/PRIVACY.md
|
||||
|
||||
And the source code here https://git.maronato.dev/maronato/cal
|
||||
|
||||
If you do not agree with (or cannot comply with) these terms, then you may not use the Service.
|
||||
|
||||
Thank you for being responsible.
|
||||
|
||||
Communications
|
||||
--------------
|
||||
|
||||
By using this service to book an event with me, you agree to receive an email with the calendar invite. You may also receive a reminder email before the event, or a confirmation email if you reschedule or cancel the event.
|
||||
|
||||
Purchases
|
||||
---------
|
||||
|
||||
There are no purchases on this service. You may use it to book events with me, but you will not be charged for it.
|
||||
|
||||
Contests, Sweepstakes and Promotions
|
||||
------------------------------------
|
||||
|
||||
There are no contests, sweepstakes, or promotions on this service.
|
||||
|
||||
Subscriptions
|
||||
-------------
|
||||
|
||||
There is no subscription on this service.
|
||||
|
||||
|
||||
Fee Changes
|
||||
-----------
|
||||
|
||||
There are no fees on this service.
|
||||
|
||||
Refunds
|
||||
-------
|
||||
|
||||
This is a free service, so there are no refunds.
|
||||
|
||||
Content
|
||||
-------
|
||||
|
||||
Our Service allows you to create an event with me by providing your name and email address. You are responsible for that information that you submit on or through Service, including its legality, reliability, and appropriateness.
|
||||
|
||||
By posting Content on or through Service, You represent and warrant that: (i) Content is yours (you own it) and/or you have the right to use it, and (ii) that the posting of your Content on or through Service does not violate the privacy rights, publicity rights, copyrights, contract rights or any other rights of any person or entity. I reserve the right to not meet with you.
|
||||
|
||||
Prohibited Uses
|
||||
---------------
|
||||
|
||||
You may use Service only for lawful purposes and in accordance with Terms. You agree not to use Service:
|
||||
|
||||
* In any way that violates any applicable national or international law or regulation.
|
||||
* For the purpose of exploiting, harming, or attempting to exploit or harm minors in any way by exposing them to inappropriate content or otherwise.
|
||||
* To transmit, or procure the sending of, any advertising or promotional material, including any “junk mail”, “chain letter,” “spam,” or any other similar solicitation.
|
||||
* To impersonate or attempt to impersonate Company, a Company employee, another user, or any other person or entity.
|
||||
* In any way that infringes upon the rights of others, or in any way is illegal, threatening, fraudulent, or harmful, or in connection with any unlawful, illegal, fraudulent, or harmful purpose or activity.
|
||||
* To engage in any other conduct that restricts or inhibits anyone’s use or enjoyment of Service, or which, as determined by us, may harm or offend Company or users of Service or expose them to liability.
|
||||
|
||||
Additionally, you agree not to:
|
||||
|
||||
* Use Service in any manner that could disable, overburden, damage, or impair Service or interfere with any other party’s use of Service, including their ability to engage in real time activities through Service.
|
||||
* Use any robot, spider, or other automatic device, process, or means to access Service for any purpose, including monitoring or copying any of the material on Service.
|
||||
* Use any manual process to monitor or copy any of the material on Service or for any other unauthorized purpose without our prior written consent.
|
||||
* Use any device, software, or routine that interferes with the proper working of Service.
|
||||
* Introduce any viruses, trojan horses, worms, logic bombs, or other material which is malicious or technologically harmful.
|
||||
* Attempt to gain unauthorized access to, interfere with, damage, or disrupt any parts of Service, the server on which Service is stored, or any server, computer, or database connected to Service.
|
||||
* Attack Service via a denial-of-service attack or a distributed denial-of-service attack.
|
||||
* Take any action that may damage or falsify Company rating.
|
||||
* Otherwise attempt to interfere with the proper working of Service.
|
||||
|
||||
Analytics
|
||||
---------
|
||||
|
||||
There is no analytics on this service.
|
||||
|
||||
No Use By Minors
|
||||
----------------
|
||||
|
||||
Service is intended only for access and use by individuals at least eighteen (18) years old. By accessing or using any of Company, you warrant and represent that you are at least eighteen (18) years of age and with the full authority, right, and capacity to enter into this agreement and abide by all of the terms and conditions of Terms. If you are not at least eighteen (18) years old, you are prohibited from both the access and usage of Service.
|
||||
|
||||
Accounts
|
||||
--------
|
||||
|
||||
You cannot create an account on this service. The only account that exists is mine.
|
||||
|
||||
Changes To Service
|
||||
------------------
|
||||
|
||||
I reserve the right to withdraw or amend this Service, and any service or material I provide via Service, in my sole discretion without notice. I will not be liable if for any reason all or any part of Service is unavailable at any time or for any period. From time to time, I may restrict access to some parts of Service, or the entire Service, to visitors.
|
||||
|
||||
Amendments To Terms
|
||||
-------------------
|
||||
|
||||
I may amend Terms at any time by posting the amended terms on this site. It is your responsibility to review these Terms periodically.
|
||||
|
||||
Acknowledgement
|
||||
---------------
|
||||
|
||||
BY USING SERVICE OR OTHER SERVICES PROVIDED BY ME, YOU ACKNOWLEDGE THAT YOU HAVE READ THESE TERMS OF SERVICE AND AGREE TO BE BOUND BY THEM.
|
||||
|
||||
Contact Me
|
||||
----------
|
||||
|
||||
If you have any questions about these terms of service, please contact me:
|
||||
By email: support@maronato.dev
|
|
@ -1,4 +0,0 @@
|
|||
# Checkly Tests
|
||||
|
||||
Run as `yarn checkly test`
|
||||
Deploy the tests as `yarn checkly deploy`
|
|
@ -1,75 +0,0 @@
|
|||
import type { Page } from "@playwright/test";
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Org", () => {
|
||||
// Because these pages involve next.config.js rewrites, it's better to test them on production
|
||||
test.describe("Embeds - i.cal.com", () => {
|
||||
test("Org Profile Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await page.screenshot({ path: "screenshot.jpg" });
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
|
||||
test("Org User(Peer) Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/peer/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator("text=Peer Richelsen")).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
|
||||
test("Org User Event(peer/meet) Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/peer/meet/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
|
||||
test("Org Team Profile(/sales) page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/sales/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator("text=Cal.com Sales")).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
|
||||
test("Org Team Event page(/sales/hippa) should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/sales/hipaa/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
|
||||
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
|
||||
async function expectPageToBeServerSideRendered(page: Page) {
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return window.__NEXT_DATA__.props.pageProps.isEmbed;
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
4
app.json
4
app.json
|
@ -83,10 +83,6 @@
|
|||
"NEXT_PUBLIC_TEAM_IMPERSONATION": {
|
||||
"description": "Set the following value to true if you wish to enable Team Impersonation",
|
||||
"value": "false"
|
||||
},
|
||||
"NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL": {
|
||||
"description": "Control time intervals on a user's Schedule availability",
|
||||
"value": "15"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
@ -48,21 +48,17 @@ Here is the full architecture:
|
|||
|
||||
### Email Router
|
||||
|
||||
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 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 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`. Set the priority to `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`, both with priority `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 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`.
|
||||
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`.
|
||||
6. Activate "POST the raw, full MIME message".
|
||||
7. Send an email to `[anyUsername]@example.com`. You should see a ping on the Tunnelmole or ngrok listener and server.
|
||||
7. Send an email to `[anyUsername]@example.com`. You should see a ping on the 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!
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
export function isValidBase64Image(input: string): boolean {
|
||||
const regex = /^data:image\/[^;]+;base64,(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
||||
return regex.test(input);
|
||||
}
|
|
@ -26,7 +26,6 @@ const schemaAvailabilityCreateParams = z
|
|||
startTime: z.date().or(z.string()),
|
||||
endTime: z.date().or(z.string()),
|
||||
days: z.array(z.number()).optional(),
|
||||
date: z.date().or(z.string()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
@ -35,7 +34,6 @@ const schemaAvailabilityEditParams = z
|
|||
startTime: z.date().or(z.string()).optional(),
|
||||
endTime: z.date().or(z.string()).optional(),
|
||||
days: z.array(z.number()).optional(),
|
||||
date: z.date().or(z.string()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { z } from "zod";
|
|||
import { _DestinationCalendarModel as DestinationCalendar } from "@calcom/prisma/zod";
|
||||
|
||||
export const schemaDestinationCalendarBaseBodyParams = DestinationCalendar.pick({
|
||||
credentialId: true,
|
||||
integration: true,
|
||||
externalId: true,
|
||||
eventTypeId: true,
|
||||
|
@ -14,6 +15,7 @@ const schemaDestinationCalendarCreateParams = z
|
|||
.object({
|
||||
integration: z.string(),
|
||||
externalId: z.string(),
|
||||
credentialId: z.number(),
|
||||
eventTypeId: z.number().optional(),
|
||||
bookingId: z.number().optional(),
|
||||
userId: z.number().optional(),
|
||||
|
@ -45,4 +47,5 @@ export const schemaDestinationCalendarReadPublic = DestinationCalendar.pick({
|
|||
eventTypeId: true,
|
||||
bookingId: true,
|
||||
userId: true,
|
||||
credentialId: true,
|
||||
});
|
||||
|
|
|
@ -60,7 +60,6 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
|
|||
successRedirectUrl: true,
|
||||
locations: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
durationLimits: true,
|
||||
})
|
||||
.merge(
|
||||
|
@ -148,7 +147,6 @@ export const schemaEventTypeReadPublic = EventType.pick({
|
|||
seatsShowAvailabilityCount: true,
|
||||
bookingFields: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
durationLimits: true,
|
||||
}).merge(
|
||||
z.object({
|
||||
|
|
|
@ -21,16 +21,7 @@ export const schemaSchedulePublic = z
|
|||
.merge(
|
||||
z.object({
|
||||
availability: z
|
||||
.array(
|
||||
Availability.pick({
|
||||
id: true,
|
||||
eventTypeId: true,
|
||||
date: true,
|
||||
days: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
})
|
||||
)
|
||||
.array(Availability.pick({ id: true, eventTypeId: true, days: true, startTime: true, endTime: true }))
|
||||
.transform((v) =>
|
||||
v.map((item) => ({
|
||||
...item,
|
||||
|
|
|
@ -4,7 +4,6 @@ import { checkUsername } from "@calcom/lib/server/checkUsername";
|
|||
import { _UserModel as User } from "@calcom/prisma/zod";
|
||||
import { iso8601 } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { isValidBase64Image } from "~/lib/utils/isValidBase64Image";
|
||||
import { timeZone } from "~/lib/validations/shared/timeZone";
|
||||
|
||||
// @note: These are the ONLY values allowed as weekStart. So user don't introduce bad data.
|
||||
|
@ -92,7 +91,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().toLowerCase(),
|
||||
email: z.string().email(),
|
||||
username: usernameSchema,
|
||||
weekStart: z.nativeEnum(weekdays).optional(),
|
||||
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||
|
@ -107,14 +106,13 @@ const schemaUserEditParams = z.object({
|
|||
.optional()
|
||||
.nullable(),
|
||||
locale: z.nativeEnum(locales).optional().nullable(),
|
||||
avatar: z.string().refine(isValidBase64Image).optional(),
|
||||
});
|
||||
|
||||
// @note: These are the values that are editable via PATCH method on the user Model,
|
||||
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
|
||||
|
||||
const schemaUserCreateParams = z.object({
|
||||
email: z.string().email().toLowerCase(),
|
||||
email: z.string().email(),
|
||||
username: usernameSchema,
|
||||
weekStart: z.nativeEnum(weekdays).optional(),
|
||||
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||
|
@ -130,7 +128,6 @@ const schemaUserCreateParams = z.object({
|
|||
.nullable(),
|
||||
locale: z.nativeEnum(locales).optional(),
|
||||
createdDate: iso8601.optional(),
|
||||
avatar: z.string().refine(isValidBase64Image).optional(),
|
||||
});
|
||||
|
||||
// @note: These are the values that are editable via PATCH method on the user Model,
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
const { withAxiom } = require("next-axiom");
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
|
||||
const plugins = [withAxiom];
|
||||
const nextConfig = {
|
||||
module.exports = withAxiom({
|
||||
transpilePackages: [
|
||||
"@calcom/app-store",
|
||||
"@calcom/core",
|
||||
|
@ -68,15 +66,4 @@ const nextConfig = {
|
|||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
"lint": "eslint . --ignore-path .gitignore",
|
||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||
"start": "PORT=3002 next start",
|
||||
"docker-start-api": "PORT=80 next start",
|
||||
"type-check": "tsc --pretty --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -30,7 +29,7 @@
|
|||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/trpc": "*",
|
||||
"@sentry/nextjs": "^7.73.0",
|
||||
"@sentry/nextjs": "^7.20.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"memory-cache": "^0.2.0",
|
||||
"next": "^13.4.6",
|
||||
|
|
|
@ -6,44 +6,18 @@ 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 userWithBookingsAndTeamIds = await prisma.user.findUnique({
|
||||
const userWithBookings = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
bookings: true,
|
||||
teams: {
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: { bookings: true },
|
||||
});
|
||||
|
||||
if (!userWithBookingsAndTeamIds) throw new HttpError({ statusCode: 404, message: "User not found" });
|
||||
if (!userWithBookings) throw new HttpError({ statusCode: 404, message: "User not found" });
|
||||
|
||||
const userBookingIds = userWithBookingsAndTeamIds.bookings.map((booking) => booking.id);
|
||||
const userBookingIds = userWithBookings.bookings.map((booking) => booking.id);
|
||||
|
||||
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" });
|
||||
}
|
||||
if (!isAdmin && !userBookingIds.includes(id)) {
|
||||
throw new HttpError({ statusCode: 401, message: "You are not authorized" });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||
* type: boolean
|
||||
* description: Delete all remaining bookings
|
||||
* - in: query
|
||||
* name: cancellationReason
|
||||
* name: reason
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
|
@ -56,6 +56,10 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||
* <td>The provided id didn't correspond to any existing booking.</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>Cannot cancel past events</td>
|
||||
* <td>The provided id matched an existing booking with a past startDate.</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>User not found</td>
|
||||
* <td>The userId did not matched an existing user.</td>
|
||||
* </tr>
|
||||
|
|
|
@ -205,8 +205,8 @@ import { defaultResponder } from "@calcom/lib/server";
|
|||
async function handler(req: NextApiRequest) {
|
||||
const { userId, isAdmin } = req;
|
||||
if (isAdmin) req.userId = req.body.userId || userId;
|
||||
|
||||
return await handleNewBooking(req);
|
||||
const booking = await handleNewBooking(req);
|
||||
return booking;
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
||||
import {
|
||||
schemaDestinationCalendarEditBodyParams,
|
||||
|
@ -62,251 +56,16 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||
* 404:
|
||||
* description: Destination calendar not found
|
||||
*/
|
||||
type DestinationCalendarType = {
|
||||
userId?: number | null;
|
||||
eventTypeId?: number | null;
|
||||
credentialId: number | null;
|
||||
};
|
||||
|
||||
type UserCredentialType = {
|
||||
id: number;
|
||||
appId: string | null;
|
||||
type: string;
|
||||
userId: number | null;
|
||||
user: {
|
||||
email: string;
|
||||
} | null;
|
||||
teamId: number | null;
|
||||
key: Prisma.JsonValue;
|
||||
invalid: boolean | null;
|
||||
};
|
||||
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin, prisma, query, body } = req;
|
||||
const { prisma, query, body } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const parsedBody = schemaDestinationCalendarEditBodyParams.parse(body);
|
||||
const assignedUserId = isAdmin ? parsedBody.userId || userId : userId;
|
||||
|
||||
validateIntegrationInput(parsedBody);
|
||||
const destinationCalendarObject: DestinationCalendarType = await getDestinationCalendar(id, prisma);
|
||||
await validateRequestAndOwnership({ destinationCalendarObject, parsedBody, assignedUserId, prisma });
|
||||
|
||||
const userCredentials = await getUserCredentials({
|
||||
credentialId: destinationCalendarObject.credentialId,
|
||||
userId: assignedUserId,
|
||||
prisma,
|
||||
});
|
||||
const credentialId = await verifyCredentialsAndGetId({
|
||||
parsedBody,
|
||||
userCredentials,
|
||||
currentCredentialId: destinationCalendarObject.credentialId,
|
||||
});
|
||||
// If the user has passed eventTypeId, we need to remove userId from the update data to make sure we don't link it to user as well
|
||||
if (parsedBody.eventTypeId) parsedBody.userId = undefined;
|
||||
const destinationCalendar = await prisma.destinationCalendar.update({
|
||||
where: { id },
|
||||
data: { ...parsedBody, credentialId },
|
||||
data: parsedBody,
|
||||
});
|
||||
return { destinationCalendar: schemaDestinationCalendarReadPublic.parse(destinationCalendar) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves user credentials associated with a given credential ID and user ID and validates if the credentials belong to this user
|
||||
*
|
||||
* @param credentialId - The ID of the credential to fetch. If not provided, an error is thrown.
|
||||
* @param userId - The user ID against which the credentials need to be verified.
|
||||
* @param prisma - An instance of PrismaClient for database operations.
|
||||
*
|
||||
* @returns - An array containing the matching user credentials.
|
||||
*
|
||||
* @throws HttpError - If `credentialId` is not provided or no associated credentials are found in the database.
|
||||
*/
|
||||
async function getUserCredentials({
|
||||
credentialId,
|
||||
userId,
|
||||
prisma,
|
||||
}: {
|
||||
credentialId: number | null;
|
||||
userId: number;
|
||||
prisma: PrismaClient;
|
||||
}) {
|
||||
if (!credentialId) {
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `Destination calendar missing credential id`,
|
||||
});
|
||||
}
|
||||
const userCredentials = await prisma.credential.findMany({
|
||||
where: { id: credentialId, userId },
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
|
||||
if (!userCredentials || userCredentials.length === 0) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: `Bad request, no associated credentials found`,
|
||||
});
|
||||
}
|
||||
return userCredentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided credentials and retrieves the associated credential ID.
|
||||
*
|
||||
* This function checks if the `integration` and `externalId` properties from the parsed body are present.
|
||||
* If both properties exist, it fetches the connected calendar credentials using the provided user credentials
|
||||
* and checks for a matching external ID and integration from the list of connected calendars.
|
||||
*
|
||||
* If a match is found, it updates the `credentialId` with the one from the connected calendar.
|
||||
* Otherwise, it throws an HTTP error with a 400 status indicating an invalid credential ID.
|
||||
*
|
||||
* If the parsed body does not contain the necessary properties, the function
|
||||
* returns the `credentialId` from the destination calendar object.
|
||||
*
|
||||
* @param parsedBody - The parsed body from the incoming request, validated against a predefined schema.
|
||||
* Checked if it contain properties like `integration` and `externalId`.
|
||||
* @param userCredentials - An array of user credentials used to fetch the connected calendar credentials.
|
||||
* @param destinationCalendarObject - An object representing the destination calendar. Primarily used
|
||||
* to fetch the default `credentialId`.
|
||||
*
|
||||
* @returns - The verified `credentialId` either from the matched connected calendar in case of updating the destination calendar,
|
||||
* or the provided destination calendar object in other cases.
|
||||
*
|
||||
* @throws HttpError - If no matching connected calendar is found for the given `integration` and `externalId`.
|
||||
*/
|
||||
async function verifyCredentialsAndGetId({
|
||||
parsedBody,
|
||||
userCredentials,
|
||||
currentCredentialId,
|
||||
}: {
|
||||
parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>;
|
||||
userCredentials: UserCredentialType[];
|
||||
currentCredentialId: number | null;
|
||||
}) {
|
||||
if (parsedBody.integration && parsedBody.externalId) {
|
||||
const calendarCredentials = getCalendarCredentials(userCredentials);
|
||||
|
||||
const { connectedCalendars } = await getConnectedCalendars(
|
||||
calendarCredentials,
|
||||
[],
|
||||
parsedBody.externalId
|
||||
);
|
||||
const eligibleCalendars = connectedCalendars[0]?.calendars?.filter((calendar) => !calendar.readOnly);
|
||||
const calendar = eligibleCalendars?.find(
|
||||
(c) => c.externalId === parsedBody.externalId && c.integration === parsedBody.integration
|
||||
);
|
||||
|
||||
if (!calendar?.credentialId)
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "Bad request, credential id invalid",
|
||||
});
|
||||
return calendar?.credentialId;
|
||||
}
|
||||
return currentCredentialId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the request for updating a destination calendar.
|
||||
*
|
||||
* This function checks the validity of the provided eventTypeId against the existing destination calendar object
|
||||
* in the sense that if the destination calendar is not linked to an event type, the eventTypeId can not be provided.
|
||||
*
|
||||
* It also ensures that the eventTypeId, if provided, belongs to the assigned user.
|
||||
*
|
||||
* @param destinationCalendarObject - An object representing the destination calendar.
|
||||
* @param parsedBody - The parsed body from the incoming request, validated against a predefined schema.
|
||||
* @param assignedUserId - The user ID assigned for the operation, which might be an admin or a regular user.
|
||||
* @param prisma - An instance of PrismaClient for database operations.
|
||||
*
|
||||
* @throws HttpError - If the validation fails or inconsistencies are detected in the request data.
|
||||
*/
|
||||
async function validateRequestAndOwnership({
|
||||
destinationCalendarObject,
|
||||
parsedBody,
|
||||
assignedUserId,
|
||||
prisma,
|
||||
}: {
|
||||
destinationCalendarObject: DestinationCalendarType;
|
||||
parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>;
|
||||
assignedUserId: number;
|
||||
prisma: PrismaClient;
|
||||
}) {
|
||||
if (parsedBody.eventTypeId) {
|
||||
if (!destinationCalendarObject.eventTypeId) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: `The provided destination calendar can not be linked to an event type`,
|
||||
});
|
||||
}
|
||||
|
||||
const userEventType = await prisma.eventType.findFirst({
|
||||
where: { id: parsedBody.eventTypeId },
|
||||
select: { userId: true },
|
||||
});
|
||||
|
||||
if (!userEventType || userEventType.userId !== assignedUserId) {
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `Event type with ID ${parsedBody.eventTypeId} not found`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsedBody.eventTypeId) {
|
||||
if (destinationCalendarObject.eventTypeId) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: `The provided destination calendar can only be linked to an event type`,
|
||||
});
|
||||
}
|
||||
if (destinationCalendarObject.userId !== assignedUserId) {
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: `Forbidden`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the destination calendar based on the provided ID as the path parameter, specifically `credentialId` and `eventTypeId`.
|
||||
*
|
||||
* If no matching destination calendar is found for the provided ID, an HTTP error with a 404 status
|
||||
* indicating that the desired destination calendar was not found is thrown.
|
||||
*
|
||||
* @param id - The ID of the destination calendar to be retrieved.
|
||||
* @param prisma - An instance of PrismaClient for database operations.
|
||||
*
|
||||
* @returns - An object containing details of the matching destination calendar, specifically `credentialId` and `eventTypeId`.
|
||||
*
|
||||
* @throws HttpError - If no destination calendar matches the provided ID.
|
||||
*/
|
||||
async function getDestinationCalendar(id: number, prisma: PrismaClient) {
|
||||
const destinationCalendarObject = await prisma.destinationCalendar.findFirst({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
select: { userId: true, eventTypeId: true, credentialId: true },
|
||||
});
|
||||
|
||||
if (!destinationCalendarObject) {
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `Destination calendar with ID ${id} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
return destinationCalendarObject;
|
||||
}
|
||||
|
||||
function validateIntegrationInput(parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>) {
|
||||
if (parsedBody.integration && !parsedBody.externalId) {
|
||||
throw new HttpError({ statusCode: 400, message: "External Id is required with integration value" });
|
||||
}
|
||||
if (!parsedBody.integration && parsedBody.externalId) {
|
||||
throw new HttpError({ statusCode: 400, message: "Integration value is required with external ID" });
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
||||
import {
|
||||
schemaDestinationCalendarReadPublic,
|
||||
|
@ -40,6 +38,9 @@ import {
|
|||
* externalId:
|
||||
* type: string
|
||||
* description: 'The external ID of the integration'
|
||||
* credentialId:
|
||||
* type: integer
|
||||
* description: 'The credential ID it is associated with'
|
||||
* eventTypeId:
|
||||
* type: integer
|
||||
* description: 'The ID of the eventType it is associated with'
|
||||
|
@ -64,38 +65,20 @@ async function postHandler(req: NextApiRequest) {
|
|||
const parsedBody = schemaDestinationCalendarCreateBodyParams.parse(body);
|
||||
await checkPermissions(req, userId);
|
||||
|
||||
const assignedUserId = isAdmin && parsedBody.userId ? parsedBody.userId : userId;
|
||||
const assignedUserId = isAdmin ? parsedBody.userId || userId : userId;
|
||||
|
||||
/* Check if credentialId data matches the ownership and integration passed in */
|
||||
const userCredentials = await prisma.credential.findMany({
|
||||
where: {
|
||||
type: parsedBody.integration,
|
||||
userId: assignedUserId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
const credential = await prisma.credential.findFirst({
|
||||
where: { type: parsedBody.integration, userId: assignedUserId },
|
||||
select: { id: true, type: true, userId: true },
|
||||
});
|
||||
|
||||
if (userCredentials.length === 0)
|
||||
if (!credential)
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "Bad request, credential id invalid",
|
||||
});
|
||||
|
||||
const calendarCredentials = getCalendarCredentials(userCredentials);
|
||||
|
||||
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, [], parsedBody.externalId);
|
||||
|
||||
const eligibleCalendars = connectedCalendars[0]?.calendars?.filter((calendar) => !calendar.readOnly);
|
||||
const calendar = eligibleCalendars?.find(
|
||||
(c) => c.externalId === parsedBody.externalId && c.integration === parsedBody.integration
|
||||
);
|
||||
if (!calendar?.credentialId)
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "Bad request, credential id invalid",
|
||||
});
|
||||
const credentialId = calendar.credentialId;
|
||||
|
||||
if (parsedBody.eventTypeId) {
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: { id: parsedBody.eventTypeId, userId: parsedBody.userId },
|
||||
|
@ -108,9 +91,7 @@ async function postHandler(req: NextApiRequest) {
|
|||
parsedBody.userId = undefined;
|
||||
}
|
||||
|
||||
const destination_calendar = await prisma.destinationCalendar.create({
|
||||
data: { ...parsedBody, credentialId },
|
||||
});
|
||||
const destination_calendar = await prisma.destinationCalendar.create({ data: { ...parsedBody } });
|
||||
|
||||
return {
|
||||
destinationCalendar: schemaDestinationCalendarReadPublic.parse(destination_calendar),
|
||||
|
|
|
@ -2,7 +2,6 @@ import type { NextApiRequest } from "next";
|
|||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
@ -52,7 +51,6 @@ export async function getHandler(req: NextApiRequest) {
|
|||
customInputs: true,
|
||||
team: { select: { slug: true } },
|
||||
users: true,
|
||||
hosts: { select: { userId: true, isFixed: true } },
|
||||
owner: { select: { username: true, id: true } },
|
||||
children: { select: { id: true, userId: true } },
|
||||
},
|
||||
|
@ -90,9 +88,7 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
|
|||
if (req.isAdmin) return true;
|
||||
if (eventType?.teamId) {
|
||||
req.query.teamId = String(eventType.teamId);
|
||||
await canAccessTeamEventOrThrow(req, {
|
||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
|
||||
});
|
||||
await canAccessTeamEventOrThrow(req, "MEMBER");
|
||||
}
|
||||
if (eventType?.userId === req.userId) return true; // is owner.
|
||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
|
|
|
@ -45,7 +45,6 @@ async function getHandler(req: NextApiRequest) {
|
|||
customInputs: true,
|
||||
team: { select: { slug: true } },
|
||||
users: true,
|
||||
hosts: { select: { userId: true, isFixed: true } },
|
||||
owner: { select: { username: true, id: true } },
|
||||
children: { select: { id: true, userId: true } },
|
||||
},
|
||||
|
|
|
@ -3,10 +3,8 @@ 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";
|
||||
|
@ -301,7 +299,7 @@ async function postHandler(req: NextApiRequest) {
|
|||
data.hosts = { createMany: { data: hosts } };
|
||||
}
|
||||
|
||||
const eventType = await prisma.eventType.create({ data, include: { hosts: true } });
|
||||
const eventType = await prisma.eventType.create({ data });
|
||||
|
||||
return {
|
||||
event_type: schemaEventTypeReadPublic.parse(eventType),
|
||||
|
@ -318,20 +316,8 @@ async function checkPermissions(req: NextApiRequest) {
|
|||
statusCode: 401,
|
||||
message: "ADMIN required for `userId`",
|
||||
});
|
||||
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`",
|
||||
});
|
||||
/* Admin users are required to pass in a userId or teamId */
|
||||
if (isAdmin && !body.userId && !body.teamId)
|
||||
throw new HttpError({ statusCode: 400, message: "`userId` or `teamId` required" });
|
||||
/* Admin users are required to pass in a userId */
|
||||
if (isAdmin && !body.userId) throw new HttpError({ statusCode: 400, message: "`userId` required" });
|
||||
}
|
||||
|
||||
export default defaultResponder(postHandler);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function CalcomApi(_: NextApiRequest, res: NextApiResponse) {
|
||||
res.status(200).json({ message: "Welcome to Cal.com API - docs are at https://developer.cal.com/api" });
|
||||
res.status(201).json({ message: "Welcome to Cal.com API - docs are at https://developer.cal.com/api" });
|
||||
}
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
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";
|
||||
|
@ -13,34 +9,10 @@ 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 { 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<string, { time: string; attendees?: number; bookingUid?: string }[]>, date) => {
|
||||
acc[date] = availableSlots.slots[date].map((slot) => ({
|
||||
...slot,
|
||||
time: dayjs(slot.time).tz(input.timeZone).format(),
|
||||
}));
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
)
|
||||
: availableSlots;
|
||||
|
||||
return slotsInProvidedTimeZone;
|
||||
const input = getScheduleSchema.parse(req.query);
|
||||
return await getAvailableSlots({ ctx: await createContext({ req, res }), input });
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (cause) {
|
||||
if (cause instanceof TRPCError) {
|
||||
|
|
|
@ -22,21 +22,7 @@ export async function checkPermissions(
|
|||
role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER
|
||||
) {
|
||||
const { userId, prisma, isAdmin } = req;
|
||||
const { teamId } = schemaQueryTeamId.parse({
|
||||
teamId: req.query.teamId,
|
||||
version: req.query.version,
|
||||
apiKey: req.query.apiKey,
|
||||
});
|
||||
return canUserAccessTeamWithRole(prisma, userId, isAdmin, teamId, role);
|
||||
}
|
||||
|
||||
export async function canUserAccessTeamWithRole(
|
||||
prisma: NextApiRequest["prisma"],
|
||||
userId: number,
|
||||
isAdmin: boolean,
|
||||
teamId: number,
|
||||
role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER
|
||||
) {
|
||||
const { teamId } = schemaQueryTeamId.parse(req.query);
|
||||
const args: Prisma.TeamFindFirstArgs = { where: { id: teamId } };
|
||||
/** If not ADMIN then we check if the actual user belongs to team and matches the required role */
|
||||
if (!isAdmin) args.where = { ...args.where, members: { some: { userId, role } } };
|
||||
|
|
|
@ -58,44 +58,12 @@ export async function patchHandler(req: NextApiRequest) {
|
|||
const { prisma, body, userId } = req;
|
||||
const data = schemaTeamUpdateBodyParams.parse(body);
|
||||
const { teamId } = schemaQueryTeamId.parse(req.query);
|
||||
|
||||
/** Only OWNERS and ADMINS can edit teams */
|
||||
const _team = await prisma.team.findFirst({
|
||||
include: { members: true },
|
||||
where: { id: teamId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
|
||||
});
|
||||
if (!_team) throw new HttpError({ statusCode: 401, message: "Unauthorized: OWNER or ADMIN required" });
|
||||
|
||||
const slugAlreadyExists = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: {
|
||||
mode: "insensitive",
|
||||
equals: data.slug,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (slugAlreadyExists && data.slug !== _team.slug)
|
||||
throw new HttpError({ statusCode: 409, message: "Team slug already exists" });
|
||||
|
||||
// Check if parentId is related to this user
|
||||
if (data.parentId && data.parentId === teamId) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "Bad request: Parent id cannot be the same as the team id.",
|
||||
});
|
||||
}
|
||||
if (data.parentId) {
|
||||
const parentTeam = await prisma.team.findFirst({
|
||||
where: { id: data.parentId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
|
||||
});
|
||||
if (!parentTeam)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "Unauthorized: Invalid parent id. You can only use parent id of your own teams.",
|
||||
});
|
||||
}
|
||||
|
||||
let paymentUrl;
|
||||
if (_team.slug === null && data.slug) {
|
||||
data.metadata = {
|
||||
|
|
|
@ -56,14 +56,6 @@ async function getHandler(req: NextApiRequest) {
|
|||
members: { some: { userId } },
|
||||
},
|
||||
},
|
||||
include: {
|
||||
customInputs: true,
|
||||
team: { select: { slug: true } },
|
||||
users: true,
|
||||
hosts: { select: { userId: true, isFixed: true } },
|
||||
owner: { select: { username: true, id: true } },
|
||||
children: { select: { id: true, userId: true } },
|
||||
},
|
||||
};
|
||||
|
||||
const data = await prisma.eventType.findMany(args);
|
||||
|
|
|
@ -68,18 +68,6 @@ async function postHandler(req: NextApiRequest) {
|
|||
}
|
||||
}
|
||||
|
||||
// Check if parentId is related to this user
|
||||
if (data.parentId) {
|
||||
const parentTeam = await prisma.team.findFirst({
|
||||
where: { id: data.parentId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
|
||||
});
|
||||
if (!parentTeam)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "Unauthorized: Invalid parent id. You can only use parent id of your own teams.",
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Perhaps there is a better fix for this?
|
||||
const cloneData: typeof data & {
|
||||
metadata: NonNullable<typeof data.metadata> | undefined;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { deleteUser } from "@calcom/features/users/lib/userDeletionService";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
|
@ -42,18 +41,10 @@ export async function deleteHandler(req: NextApiRequest) {
|
|||
// Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user
|
||||
if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: query.userId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
metadata: true,
|
||||
},
|
||||
});
|
||||
const user = await prisma.user.findUnique({ where: { id: query.userId } });
|
||||
if (!user) throw new HttpError({ statusCode: 404, message: "User not found" });
|
||||
|
||||
await deleteUser(user);
|
||||
|
||||
await prisma.user.delete({ where: { id: user.id } });
|
||||
return { message: `User with id: ${user.id} deleted successfully` };
|
||||
}
|
||||
|
||||
|
|
|
@ -65,9 +65,6 @@ import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validation
|
|||
* locale:
|
||||
* description: The user's locale. Acceptable values are one of [EN, FR, IT, RU, ES, DE, PT, RO, NL, PT_BR, ES_419, KO, JA, PL, AR, IW, ZH_CH, ZH_TW, CS, SR, SV, VI]
|
||||
* type: string
|
||||
* avatar:
|
||||
* description: The user's avatar, in base64 format
|
||||
* type: string
|
||||
* examples:
|
||||
* user:
|
||||
* summary: An example of USER
|
||||
|
|
|
@ -60,9 +60,6 @@ import { schemaUserCreateBodyParams } from "~/lib/validations/user";
|
|||
* locale:
|
||||
* description: The new user's locale. Acceptable values are one of [EN, FR, IT, RU, ES, DE, PT, RO, NL, PT_BR, ES_419, KO, JA, PL, AR, IW, ZH_CH, ZH_TW, CS, SR, SV, VI]
|
||||
* type: string
|
||||
* avatar:
|
||||
* description: The user's avatar, in base64 format
|
||||
* type: string
|
||||
* examples:
|
||||
* user:
|
||||
* summary: An example of USER
|
||||
|
|
|
@ -69,12 +69,7 @@ import { schemaWebhookEditBodyParams, schemaWebhookReadPublic } from "~/lib/vali
|
|||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { prisma, query, userId, isAdmin } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(query);
|
||||
const {
|
||||
eventTypeId,
|
||||
userId: bodyUserId,
|
||||
eventTriggers,
|
||||
...data
|
||||
} = schemaWebhookEditBodyParams.parse(req.body);
|
||||
const { eventTypeId, userId: bodyUserId, ...data } = schemaWebhookEditBodyParams.parse(req.body);
|
||||
const args: Prisma.WebhookUpdateArgs = { where: { id }, data };
|
||||
|
||||
if (eventTypeId) {
|
||||
|
@ -92,11 +87,6 @@ export async function patchHandler(req: NextApiRequest) {
|
|||
args.data.userId = bodyUserId;
|
||||
}
|
||||
|
||||
if (args.data.eventTriggers) {
|
||||
const eventTriggersSet = new Set(eventTriggers);
|
||||
args.data.eventTriggers = Array.from(eventTriggersSet);
|
||||
}
|
||||
|
||||
const result = await prisma.webhook.update(args);
|
||||
return { webhook: schemaWebhookReadPublic.parse(result) };
|
||||
}
|
||||
|
|
|
@ -66,12 +66,7 @@ import { schemaWebhookCreateBodyParams, schemaWebhookReadPublic } from "~/lib/va
|
|||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin, prisma } = req;
|
||||
const {
|
||||
eventTypeId,
|
||||
userId: bodyUserId,
|
||||
eventTriggers,
|
||||
...body
|
||||
} = schemaWebhookCreateBodyParams.parse(req.body);
|
||||
const { eventTypeId, userId: bodyUserId, ...body } = schemaWebhookCreateBodyParams.parse(req.body);
|
||||
const args: Prisma.WebhookCreateArgs = { data: { id: uuidv4(), ...body } };
|
||||
|
||||
// If no event type, we assume is for the current user. If admin we run more checks below...
|
||||
|
@ -92,11 +87,6 @@ async function postHandler(req: NextApiRequest) {
|
|||
args.data.userId = bodyUserId;
|
||||
}
|
||||
|
||||
if (args.data.eventTriggers) {
|
||||
const eventTriggersSet = new Set(eventTriggers);
|
||||
args.data.eventTriggers = Array.from(eventTriggersSet);
|
||||
}
|
||||
|
||||
const data = await prisma.webhook.create(args);
|
||||
|
||||
return {
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export {};
|
|
@ -1,5 +0,0 @@
|
|||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
|
@ -8,7 +8,6 @@ import { describe, expect, test, vi } from "vitest";
|
|||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
|
@ -149,7 +148,7 @@ describe.skipIf(true)("POST /api/bookings", () => {
|
|||
expect(res._getStatusCode()).toBe(500);
|
||||
expect(JSON.parse(res._getData())).toEqual(
|
||||
expect.objectContaining({
|
||||
message: ErrorCode.NoAvailableUsersFound,
|
||||
message: "No available users found.",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"functions": {
|
||||
"pages/api/slots/*.ts": {
|
||||
"memory": 512
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,9 +8,7 @@ pnpm-debug.log*
|
|||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
storybook-static/*
|
||||
!storybook-static/favicon.ico
|
||||
!storybook-static/sb-cover.jpg
|
||||
storybook-static
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
stories: [
|
||||
"../intro.stories.mdx",
|
||||
"../../../packages/ui/components/**/*.stories.mdx",
|
||||
"../../../packages/atoms/**/*.stories.mdx",
|
||||
"../../../packages/features/**/*.stories.mdx",
|
||||
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions",
|
||||
"storybook-addon-rtl-direction",
|
||||
"storybook-react-i18next",
|
||||
"storybook-addon-next",
|
||||
"storybook-addon-next-router",
|
||||
/*{
|
||||
name: "storybook-addon-next",
|
||||
options: {
|
||||
nextConfigPath: path.resolve(__dirname, "../../web/next.config.js"),
|
||||
},
|
||||
},*/
|
||||
],
|
||||
framework: "@storybook/react",
|
||||
core: {
|
||||
builder: "webpack5",
|
||||
},
|
||||
staticDirs: ["../public"],
|
||||
webpackFinal: async (config, { configType }) => {
|
||||
config.resolve.fallback = {
|
||||
fs: false,
|
||||
assert: false,
|
||||
buffer: false,
|
||||
console: false,
|
||||
constants: false,
|
||||
crypto: false,
|
||||
domain: false,
|
||||
events: false,
|
||||
http: false,
|
||||
https: false,
|
||||
os: false,
|
||||
path: false,
|
||||
punycode: false,
|
||||
process: false,
|
||||
querystring: false,
|
||||
stream: false,
|
||||
string_decoder: false,
|
||||
sys: false,
|
||||
timers: false,
|
||||
tty: false,
|
||||
url: false,
|
||||
util: false,
|
||||
vm: false,
|
||||
zlib: false,
|
||||
};
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
"style-loader",
|
||||
{
|
||||
loader: "css-loader",
|
||||
options: {
|
||||
modules: true, // Enable modules to help you using className
|
||||
},
|
||||
},
|
||||
],
|
||||
include: path.resolve(__dirname, "../src"),
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
typescript: { reactDocgen: "react-docgen" },
|
||||
};
|
|
@ -1,96 +0,0 @@
|
|||
import type { StorybookConfig } from "@storybook/nextjs";
|
||||
import path, { dirname, join } from "path";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
"../intro.stories.mdx",
|
||||
"../../../packages/ui/components/**/*.stories.mdx", // legacy SB6 stories
|
||||
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../../../packages/ui/components/**/*.docs.mdx",
|
||||
"../../../packages/features/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../../../packages/features/**/*.docs.mdx",
|
||||
"../../../packages/atoms/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../../../packages/atoms/**/*.docs.mdx",
|
||||
],
|
||||
|
||||
addons: [
|
||||
getAbsolutePath("@storybook/addon-links"),
|
||||
getAbsolutePath("@storybook/addon-essentials"),
|
||||
getAbsolutePath("@storybook/addon-interactions"),
|
||||
getAbsolutePath("storybook-addon-rtl-direction"),
|
||||
getAbsolutePath("storybook-react-i18next"),
|
||||
],
|
||||
|
||||
framework: {
|
||||
name: getAbsolutePath("@storybook/nextjs") as "@storybook/nextjs",
|
||||
|
||||
options: {
|
||||
// builder: {
|
||||
// fsCache: true,
|
||||
// lazyCompilation: true,
|
||||
// },
|
||||
},
|
||||
},
|
||||
|
||||
staticDirs: ["../public"],
|
||||
|
||||
webpackFinal: async (config, { configType }) => {
|
||||
config.resolve = config.resolve || {};
|
||||
config.resolve.fallback = {
|
||||
fs: false,
|
||||
assert: false,
|
||||
buffer: false,
|
||||
console: false,
|
||||
constants: false,
|
||||
crypto: false,
|
||||
domain: false,
|
||||
events: false,
|
||||
http: false,
|
||||
https: false,
|
||||
os: false,
|
||||
path: false,
|
||||
punycode: false,
|
||||
process: false,
|
||||
querystring: false,
|
||||
stream: false,
|
||||
string_decoder: false,
|
||||
sys: false,
|
||||
timers: false,
|
||||
tty: false,
|
||||
url: false,
|
||||
util: false,
|
||||
vm: false,
|
||||
zlib: false,
|
||||
};
|
||||
|
||||
config.module = config.module || {};
|
||||
config.module.rules = config.module.rules || [];
|
||||
config.module.rules.push({
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
"style-loader",
|
||||
{
|
||||
loader: "css-loader",
|
||||
options: {
|
||||
modules: true, // Enable modules to help you using className
|
||||
},
|
||||
},
|
||||
],
|
||||
include: path.resolve(__dirname, "../src"),
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
typescript: { reactDocgen: "react-docgen" },
|
||||
|
||||
docs: {
|
||||
autodocs: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
function getAbsolutePath(value) {
|
||||
return dirname(require.resolve(join(value, "package.json")));
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import { addDecorator } from "@storybook/react";
|
||||
import { AppRouterContext } from "next/dist/shared/lib/app-router-context";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
|
||||
import "../styles/globals.css";
|
||||
import "../styles/storybook-styles.css";
|
||||
import i18n from "./i18next";
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
nextRouter: {
|
||||
pathname: "/",
|
||||
asPath: "/",
|
||||
query: {},
|
||||
push() {},
|
||||
Provider: AppRouterContext.Provider,
|
||||
},
|
||||
globals: {
|
||||
locale: "en",
|
||||
locales: {
|
||||
en: "English",
|
||||
fr: "Français",
|
||||
},
|
||||
},
|
||||
i18n,
|
||||
};
|
||||
|
||||
addDecorator((storyFn) => (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<div style={{ margin: "2rem" }}>{storyFn()}</div>
|
||||
</I18nextProvider>
|
||||
));
|
||||
|
||||
window.getEmbedNamespace = () => {
|
||||
const url = new URL(document.URL);
|
||||
const namespace = url.searchParams.get("embed");
|
||||
return namespace;
|
||||
};
|
||||
|
||||
window.getEmbedTheme = () => {
|
||||
return "auto";
|
||||
};
|
|
@ -1,73 +0,0 @@
|
|||
// adds tooltip context to all stories
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import type { Preview } from "@storybook/react";
|
||||
import React from "react";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
|
||||
import type { EmbedThemeConfig } from "@calcom/embed-core/src/types";
|
||||
// adds trpc context to all stories (esp. booker)
|
||||
import { StorybookTrpcProvider } from "@calcom/ui";
|
||||
|
||||
import "../styles/globals.css";
|
||||
import "../styles/storybook-styles.css";
|
||||
import i18n from "./i18next";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
|
||||
globals: {
|
||||
locale: "en",
|
||||
locales: {
|
||||
en: "English",
|
||||
fr: "Français",
|
||||
},
|
||||
},
|
||||
|
||||
i18n,
|
||||
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<StorybookTrpcProvider>
|
||||
<TooltipProvider>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<div style={{ margin: "2rem" }}>
|
||||
<Story />
|
||||
</div>
|
||||
</I18nextProvider>
|
||||
</TooltipProvider>
|
||||
</StorybookTrpcProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
getEmbedNamespace: () => string | null;
|
||||
getEmbedTheme: () => EmbedThemeConfig | null;
|
||||
}
|
||||
}
|
||||
|
||||
window.getEmbedNamespace = () => {
|
||||
const url = new URL(document.URL);
|
||||
const namespace = url.searchParams.get("embed");
|
||||
return namespace;
|
||||
};
|
||||
|
||||
window.getEmbedTheme = () => {
|
||||
return "auto";
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { ArgsTable } from "@storybook/addon-docs";
|
||||
import type { SortType } from "@storybook/blocks";
|
||||
import type { PropDescriptor } from "@storybook/preview-api";
|
||||
import { SortType } from "@storybook/components";
|
||||
import { PropDescriptor } from "@storybook/store";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore storybook addon types component as any so we have to do
|
||||
type Component = any;
|
||||
|
|
|
@ -9,7 +9,10 @@ import { Meta } from "@storybook/addon-docs";
|
|||
library, we will be adding more components as we go along.
|
||||
</p>
|
||||
<p>
|
||||
Our <a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">Figma</a>
|
||||
Our{" "}
|
||||
<a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">
|
||||
Figma
|
||||
</a>{" "}
|
||||
library is available for anyone to view and use. If you have any questions or concerns, please reach out to
|
||||
the design team.
|
||||
</p>
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "storybook dev -p 6006",
|
||||
"build": "storybook build"
|
||||
"dev": "start-storybook -p 6006",
|
||||
"build": "build-storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/config": "*",
|
||||
"@calcom/dayjs": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-avatar": "^1.0.0",
|
||||
"@radix-ui/react-collapsible": "^1.0.0",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
|
@ -20,25 +20,23 @@
|
|||
"@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": "^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",
|
||||
"@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",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
|
@ -48,9 +46,10 @@
|
|||
"postcss": "^8.4.18",
|
||||
"postcss-pseudo-companion-classes": "^0.1.1",
|
||||
"rollup-plugin-polyfill-node": "^0.10.2",
|
||||
"storybook": "^7.6.3",
|
||||
"storybook-react-i18next": "^2.0.9",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"storybook-addon-designs": "^6.3.1",
|
||||
"storybook-addon-next": "^1.6.9",
|
||||
"storybook-react-i18next": "^1.1.2",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.1.2"
|
||||
}
|
||||
|
|
|
@ -197,91 +197,88 @@
|
|||
@layer {
|
||||
:root {
|
||||
/* background */
|
||||
|
||||
--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);
|
||||
|
||||
|
||||
--cal-bg-emphasis: #e5e7eb;
|
||||
--cal-bg: white;
|
||||
--cal-bg-subtle: #f3f4f6;
|
||||
--cal-bg-muted: #f9fafb;
|
||||
--cal-bg-inverted: #111827;
|
||||
|
||||
/* background -> components*/
|
||||
--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);
|
||||
|
||||
--cal-bg-info: #dee9fc;
|
||||
--cal-bg-success: #e2fbe8;
|
||||
--cal-bg-attention: #fceed8;
|
||||
--cal-bg-error: #f9e3e2;
|
||||
--cal-bg-dark-error: #752522;
|
||||
|
||||
/* Borders */
|
||||
--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);
|
||||
|
||||
--cal-border-emphasis: #9ca3af;
|
||||
--cal-border: #d1d5db;
|
||||
--cal-border-subtle: #e5e7eb;
|
||||
--cal-border-muted: #f3f4f6;
|
||||
--cal-border-error: #aa2e26;
|
||||
|
||||
/* Content/Text */
|
||||
--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);
|
||||
|
||||
--cal-text-emphasis: #111827;
|
||||
--cal-text: #374151;
|
||||
--cal-text-subtle: #6b7280;
|
||||
--cal-text-muted: #9ca3af;
|
||||
--cal-text-inverted: white;
|
||||
|
||||
/* Content/Text -> components */
|
||||
--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);
|
||||
|
||||
--cal-text-info: #253985;
|
||||
--cal-text-success: #285231;
|
||||
--cal-text-attention: #73321b;
|
||||
--cal-text-error: #752522;
|
||||
|
||||
/* Brand shinanigans
|
||||
-> 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);
|
||||
-> These will be computed for the users theme at runtime.
|
||||
*/
|
||||
--cal-brand: #111827;
|
||||
--cal-brand-emphasis: #101010;
|
||||
--cal-brand-text: white;
|
||||
}
|
||||
.dark {
|
||||
/* background */
|
||||
|
||||
--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);
|
||||
|
||||
|
||||
--cal-bg-emphasis: #2b2b2b;
|
||||
--cal-bg: #101010;
|
||||
--cal-bg-subtle: #2b2b2b;
|
||||
--cal-bg-muted: #1c1c1c;
|
||||
--cal-bg-inverted: #f3f4f6;
|
||||
|
||||
/* background -> components*/
|
||||
--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);
|
||||
|
||||
--cal-bg-info: #263fa9;
|
||||
--cal-bg-success: #306339;
|
||||
--cal-bg-attention: #8e3b1f;
|
||||
--cal-bg-error: #8c2822;
|
||||
--cal-bg-dark-error: #752522;
|
||||
|
||||
/* Borders */
|
||||
--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);
|
||||
|
||||
--cal-border-emphasis: #575757;
|
||||
--cal-border: #444444;
|
||||
--cal-border-subtle: #2b2b2b;
|
||||
--cal-border-muted: #1c1c1c;
|
||||
--cal-border-error: #aa2e26;
|
||||
|
||||
/* Content/Text */
|
||||
--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);
|
||||
|
||||
--cal-text-emphasis: #f3f4f6;
|
||||
--cal-text: #d6d6d6;
|
||||
--cal-text-subtle: #757575;
|
||||
--cal-text-muted: #575757;
|
||||
--cal-text-inverted: #101010;
|
||||
|
||||
/* Content/Text -> components */
|
||||
--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);
|
||||
|
||||
--cal-text-info: #dee9fc;
|
||||
--cal-text-success: #e2fbe8;
|
||||
--cal-text-attention: #fceed8;
|
||||
--cal-text-error: #f9e3e2;
|
||||
|
||||
/* Brand shenanigans
|
||||
-> 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);
|
||||
-> These will be computed for the users theme at runtime.
|
||||
*/
|
||||
--cal-brand: white;
|
||||
--cal-brand-emphasis: #e1e1e1;
|
||||
--cal-brand-text: black;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
import { getBucket } from "abTest/utils";
|
||||
import type { NextMiddleware, NextRequest } from "next/server";
|
||||
import { NextResponse, URLPattern } from "next/server";
|
||||
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,
|
||||
}),
|
||||
enabled,
|
||||
]);
|
||||
|
||||
const FUTURE_ROUTES_OVERRIDE_COOKIE_NAME = "x-calcom-future-routes-override";
|
||||
const FUTURE_ROUTES_ENABLED_COOKIE_NAME = "x-calcom-future-routes-enabled";
|
||||
|
||||
const bucketSchema = z.union([z.literal("legacy"), z.literal("future")]);
|
||||
|
||||
export const abTestMiddlewareFactory =
|
||||
(next: (req: NextRequest) => Promise<NextResponse<unknown>>): NextMiddleware =>
|
||||
async (req: NextRequest) => {
|
||||
const response = await next(req);
|
||||
|
||||
const { pathname } = req.nextUrl;
|
||||
|
||||
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) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const safeParsedBucket = override
|
||||
? { success: true as const, data: "future" as const }
|
||||
: bucketSchema.safeParse(req.cookies.get(FUTURE_ROUTES_ENABLED_COOKIE_NAME)?.value);
|
||||
|
||||
if (!safeParsedBucket.success) {
|
||||
// cookie does not exist or it has incorrect value
|
||||
const bucket = getBucket();
|
||||
|
||||
response.cookies.set(FUTURE_ROUTES_ENABLED_COOKIE_NAME, bucket, {
|
||||
expires: Date.now() + 1000 * 60 * 30,
|
||||
httpOnly: true,
|
||||
}); // 30 min in ms
|
||||
|
||||
if (bucket === "legacy") {
|
||||
return response;
|
||||
}
|
||||
|
||||
const url = req.nextUrl.clone();
|
||||
url.pathname = `future${pathname}/`;
|
||||
|
||||
return NextResponse.rewrite(url, response);
|
||||
}
|
||||
|
||||
if (safeParsedBucket.data === "legacy") {
|
||||
return response;
|
||||
}
|
||||
|
||||
const url = req.nextUrl.clone();
|
||||
url.pathname = `future${pathname}/`;
|
||||
|
||||
return NextResponse.rewrite(url, response);
|
||||
};
|
|
@ -1,9 +0,0 @@
|
|||
import { AB_TEST_BUCKET_PROBABILITY } from "@calcom/lib/constants";
|
||||
|
||||
const cryptoRandom = () => {
|
||||
return crypto.getRandomValues(new Uint8Array(1))[0] / 0xff;
|
||||
};
|
||||
|
||||
export const getBucket = () => {
|
||||
return cryptoRandom() * 100 < AB_TEST_BUCKET_PROBABILITY ? "future" : "legacy";
|
||||
};
|
|
@ -1,21 +0,0 @@
|
|||
import type { GetServerSideProps, GetServerSidePropsContext } from "next";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
export const withAppDir =
|
||||
<T extends Record<string, any>>(getServerSideProps: GetServerSideProps<T>) =>
|
||||
async (context: GetServerSidePropsContext): Promise<T> => {
|
||||
const ssrResponse = await getServerSideProps(context);
|
||||
|
||||
if ("redirect" in ssrResponse) {
|
||||
redirect(ssrResponse.redirect.destination);
|
||||
}
|
||||
if ("notFound" in ssrResponse) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
...ssrResponse.props,
|
||||
// includes dehydratedState required for future page trpcPropvider
|
||||
...("trpcState" in ssrResponse.props && { dehydratedState: ssrResponse.props.trpcState }),
|
||||
};
|
||||
};
|
|
@ -1,57 +0,0 @@
|
|||
import type { GetServerSidePropsContext } from "next";
|
||||
import { isNotFoundError } from "next/dist/client/components/not-found";
|
||||
import { getURLFromRedirectError, isRedirectError } from "next/dist/client/components/redirect";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
export type EmbedProps = {
|
||||
isEmbed?: boolean;
|
||||
};
|
||||
|
||||
export default function withEmbedSsrAppDir<T extends Record<string, any>>(
|
||||
getData: (context: GetServerSidePropsContext) => Promise<T>
|
||||
) {
|
||||
return async (context: GetServerSidePropsContext): Promise<T> => {
|
||||
const { embed, layout } = context.query;
|
||||
|
||||
try {
|
||||
const props = await getData(context);
|
||||
|
||||
return {
|
||||
...props,
|
||||
isEmbed: true,
|
||||
};
|
||||
} catch (e) {
|
||||
if (isRedirectError(e)) {
|
||||
const destinationUrl = getURLFromRedirectError(e);
|
||||
let urlPrefix = "";
|
||||
|
||||
// Get the URL parsed from URL so that we can reliably read pathname and searchParams from it.
|
||||
const destinationUrlObj = new URL(destinationUrl, WEBAPP_URL);
|
||||
|
||||
// If it's a complete URL, use the origin as the prefix to ensure we redirect to the same domain.
|
||||
if (destinationUrl.search(/^(http:|https:).*/) !== -1) {
|
||||
urlPrefix = destinationUrlObj.origin;
|
||||
} else {
|
||||
// Don't use any prefix for relative URLs to ensure we stay on the same domain
|
||||
urlPrefix = "";
|
||||
}
|
||||
|
||||
const destinationQueryStr = destinationUrlObj.searchParams.toString();
|
||||
// Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace
|
||||
const newDestinationUrl = `${urlPrefix}${destinationUrlObj.pathname}/embed?${
|
||||
destinationQueryStr ? `${destinationQueryStr}&` : ""
|
||||
}layout=${layout}&embed=${embed}`;
|
||||
|
||||
redirect(newDestinationUrl);
|
||||
}
|
||||
|
||||
if (isNotFoundError(e)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { createHydrateClient } from "app/_trpc/createHydrateClient";
|
||||
import superjson from "superjson";
|
||||
|
||||
export const HydrateClient = createHydrateClient({
|
||||
transformer: superjson,
|
||||
});
|
|
@ -1,5 +0,0 @@
|
|||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
|
||||
import { createTRPCReact } from "@trpc/react-query";
|
||||
|
||||
export const trpc = createTRPCReact<AppRouter>({});
|
|
@ -1,21 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { type DehydratedState, Hydrate } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import type { DataTransformer } from "@trpc/server";
|
||||
|
||||
export function createHydrateClient(opts: { transformer?: DataTransformer }) {
|
||||
return function HydrateClient(props: { children: React.ReactNode; state: DehydratedState }) {
|
||||
const { state, children } = props;
|
||||
|
||||
const transformedState: DehydratedState = useMemo(() => {
|
||||
if (opts.transformer) {
|
||||
return opts.transformer.deserialize(state);
|
||||
}
|
||||
return state;
|
||||
}, [state]);
|
||||
|
||||
return <Hydrate state={transformedState}>{children}</Hydrate>;
|
||||
};
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
import type { TRPCContext } from "@calcom/trpc/server/createContext";
|
||||
import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||
|
||||
export const getServerCaller = (ctx: TRPCContext) => appRouter.createCaller(ctx);
|
|
@ -1,103 +0,0 @@
|
|||
import { type DehydratedState, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { HydrateClient } from "app/_trpc/HydrateClient";
|
||||
import { trpc } from "app/_trpc/client";
|
||||
import { useState } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
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";
|
||||
|
||||
export type Endpoint = (typeof ENDPOINTS)[number];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const resolveEndpoint = (links: any) => {
|
||||
// TODO: Update our trpc routes so they are more clear.
|
||||
// This function parses paths like the following and maps them
|
||||
// to the correct API endpoints.
|
||||
// - viewer.me - 2 segment paths like this are for logged in requests
|
||||
// - viewer.public.i18n - 3 segments paths can be public or authed
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (ctx: any) => {
|
||||
const parts = ctx.op.path.split(".");
|
||||
let endpoint;
|
||||
let path = "";
|
||||
if (parts.length == 2) {
|
||||
endpoint = parts[0] as keyof typeof links;
|
||||
path = parts[1];
|
||||
} else {
|
||||
endpoint = parts[1] as keyof typeof links;
|
||||
path = parts.splice(2, parts.length - 2).join(".");
|
||||
}
|
||||
return links[endpoint]({ ...ctx, op: { ...ctx.op, path } });
|
||||
};
|
||||
};
|
||||
|
||||
export const TrpcProvider: React.FC<{ children: React.ReactNode; dehydratedState?: DehydratedState }> = ({
|
||||
children,
|
||||
dehydratedState,
|
||||
}) => {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: { queries: { staleTime: 5000 } },
|
||||
})
|
||||
);
|
||||
const url =
|
||||
typeof window !== "undefined"
|
||||
? "/api/trpc"
|
||||
: process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}/api/trpc`
|
||||
: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;
|
||||
|
||||
const [trpcClient] = useState(() =>
|
||||
trpc.createClient({
|
||||
links: [
|
||||
// adds pretty logs to your console in development and logs errors in production
|
||||
loggerLink({
|
||||
enabled: (opts) =>
|
||||
!!process.env.NEXT_PUBLIC_DEBUG || (opts.direction === "down" && opts.result instanceof Error),
|
||||
}),
|
||||
splitLink({
|
||||
// check for context property `skipBatch`
|
||||
condition: (op) => !!op.context.skipBatch,
|
||||
// when condition is true, use normal request
|
||||
true: (runtime) => {
|
||||
const links = Object.fromEntries(
|
||||
ENDPOINTS.map((endpoint) => [
|
||||
endpoint,
|
||||
httpLink({
|
||||
url: `${url}/${endpoint}`,
|
||||
})(runtime),
|
||||
])
|
||||
);
|
||||
return resolveEndpoint(links);
|
||||
},
|
||||
// when condition is false, use batch request
|
||||
false: (runtime) => {
|
||||
const links = Object.fromEntries(
|
||||
ENDPOINTS.map((endpoint) => [
|
||||
endpoint,
|
||||
httpBatchLink({
|
||||
url: `${url}/${endpoint}`,
|
||||
})(runtime),
|
||||
])
|
||||
);
|
||||
return resolveEndpoint(links);
|
||||
},
|
||||
}),
|
||||
],
|
||||
transformer: superjson,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{dehydratedState ? <HydrateClient state={dehydratedState}>{children}</HydrateClient> : children}
|
||||
</QueryClientProvider>
|
||||
</trpc.Provider>
|
||||
);
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
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 };
|
|
@ -1,40 +0,0 @@
|
|||
import { type TFunction } from "i18next";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { constructGenericImage } from "@calcom/lib/OgImages";
|
||||
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG } from "@calcom/lib/constants";
|
||||
import { getFixedT } from "@calcom/lib/server/getFixedT";
|
||||
|
||||
import { preparePageMetadata } from "@lib/metadata";
|
||||
|
||||
export const _generateMetadata = async (
|
||||
getTitle: (t: TFunction<string, undefined>) => string,
|
||||
getDescription: (t: TFunction<string, undefined>) => string
|
||||
) => {
|
||||
const h = headers();
|
||||
const canonical = h.get("x-pathname") ?? "";
|
||||
const locale = h.get("x-locale") ?? "en";
|
||||
|
||||
const t = await getFixedT(locale, "common");
|
||||
|
||||
const title = getTitle(t);
|
||||
const description = getDescription(t);
|
||||
|
||||
const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL);
|
||||
|
||||
const image =
|
||||
SEO_IMG_OGIMG +
|
||||
constructGenericImage({
|
||||
title,
|
||||
description,
|
||||
});
|
||||
|
||||
return preparePageMetadata({
|
||||
title,
|
||||
canonical,
|
||||
image,
|
||||
description,
|
||||
siteName: APP_NAME,
|
||||
metadataBase,
|
||||
});
|
||||
};
|
|
@ -1,64 +0,0 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* Typescript class based component for custom-error
|
||||
* @link https://nextjs.org/docs/advanced-features/custom-error-page
|
||||
*/
|
||||
import type { NextPage } from "next";
|
||||
import type { ErrorProps } from "next/error";
|
||||
import React from "react";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { redactError } from "@calcom/lib/redactError";
|
||||
|
||||
import { ErrorPage } from "@components/error/error-page";
|
||||
|
||||
type NextError = Error & { digest?: string };
|
||||
|
||||
// Ref: https://nextjs.org/docs/app/api-reference/file-conventions/error#props
|
||||
export type DefaultErrorProps = {
|
||||
error: NextError;
|
||||
reset: () => void; // A function to reset the error boundary
|
||||
};
|
||||
|
||||
type AugmentedError = NextError | HttpError | null;
|
||||
|
||||
type CustomErrorProps = {
|
||||
err?: AugmentedError;
|
||||
statusCode?: number;
|
||||
message?: string;
|
||||
} & Omit<ErrorProps, "err" | "statusCode">;
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[error]"] });
|
||||
|
||||
const CustomError: NextPage<DefaultErrorProps> = (props) => {
|
||||
const { error } = props;
|
||||
let errorObject: CustomErrorProps = {
|
||||
message: error.message,
|
||||
err: error,
|
||||
};
|
||||
|
||||
if (error instanceof HttpError) {
|
||||
const redactedError = redactError(error);
|
||||
errorObject = {
|
||||
statusCode: error.statusCode,
|
||||
title: redactedError.name,
|
||||
message: redactedError.message,
|
||||
err: {
|
||||
...redactedError,
|
||||
...error,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// `error.digest` property contains an automatically generated hash of the error that can be used to match the corresponding error in server-side logs
|
||||
log.debug(`${error?.toString() ?? JSON.stringify(error)}`);
|
||||
log.info("errorObject: ", errorObject);
|
||||
|
||||
return (
|
||||
<ErrorPage statusCode={errorObject.statusCode} error={errorObject.err} message={errorObject.message} />
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomError;
|
|
@ -1,3 +0,0 @@
|
|||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
export default WithLayout({ getLayout: null })<"L">;
|
|
@ -1,126 +0,0 @@
|
|||
import AppPage from "@pages/apps/[slug]/index";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import fs from "fs";
|
||||
import matter from "gray-matter";
|
||||
import { notFound } from "next/navigation";
|
||||
import path from "path";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||
import { getAppAssetFullPath } from "@calcom/app-store/getAppAssetFullPath";
|
||||
import { APP_NAME, IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const sourceSchema = z.object({
|
||||
content: z.string(),
|
||||
data: z.object({
|
||||
description: z.string().optional(),
|
||||
items: z
|
||||
.array(
|
||||
z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
iframe: z.object({ src: z.string() }),
|
||||
}),
|
||||
])
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const { data } = await getPageProps({ params });
|
||||
|
||||
return await _generateMetadata(
|
||||
() => `${data.name} | ${APP_NAME}`,
|
||||
() => data.description
|
||||
);
|
||||
};
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
try {
|
||||
const appStore = await prisma.app.findMany({ select: { slug: true } });
|
||||
return appStore.map(({ slug }) => ({ slug }));
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Prisma.PrismaClientInitializationError) {
|
||||
// Database is not available at build time, but that's ok – we fall back to resolving paths on demand
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
if (typeof params?.slug !== "string") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const appMeta = await getAppWithMetadata({
|
||||
slug: params?.slug,
|
||||
});
|
||||
|
||||
const appFromDb = await prisma.app.findUnique({
|
||||
where: { slug: params.slug.toLowerCase() },
|
||||
});
|
||||
|
||||
const isAppAvailableInFileSystem = appMeta;
|
||||
const isAppDisabled = isAppAvailableInFileSystem && (!appFromDb || !appFromDb.enabled);
|
||||
|
||||
if (!IS_PRODUCTION && isAppDisabled) {
|
||||
return {
|
||||
isAppDisabled: true as const,
|
||||
data: {
|
||||
...appMeta,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!appFromDb || !appMeta || isAppDisabled) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const isTemplate = appMeta.isTemplate;
|
||||
const appDirname = path.join(isTemplate ? "templates" : "", appFromDb.dirName);
|
||||
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
|
||||
const postFilePath = path.join(README_PATH);
|
||||
let source = "";
|
||||
|
||||
try {
|
||||
source = fs.readFileSync(postFilePath).toString();
|
||||
source = source.replace(/{DESCRIPTION}/g, appMeta.description);
|
||||
} catch (error) {
|
||||
/* If the app doesn't have a README we fallback to the package description */
|
||||
console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
|
||||
source = appMeta.description;
|
||||
}
|
||||
|
||||
const result = matter(source);
|
||||
const { content, data } = sourceSchema.parse({ content: result.content, data: result.data });
|
||||
if (data.items) {
|
||||
data.items = data.items.map((item) => {
|
||||
if (typeof item === "string") {
|
||||
return getAppAssetFullPath(item, {
|
||||
dirName: appMeta.dirName,
|
||||
isTemplate: appMeta.isTemplate,
|
||||
});
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return {
|
||||
isAppDisabled: false as const,
|
||||
source: { content, data },
|
||||
data: appMeta,
|
||||
};
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||
const pageProps = await getPageProps({ params });
|
||||
|
||||
return <AppPage {...pageProps} />;
|
||||
}
|
||||
|
||||
export const dynamic = "force-static";
|
|
@ -1,36 +0,0 @@
|
|||
import SetupPage from "@pages/apps/[slug]/setup";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
return await _generateMetadata(
|
||||
() => `${params.slug} | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const req = { headers: headers(), cookies: cookies() };
|
||||
|
||||
const result = await getServerSideProps({ params, req } as unknown as GetServerSidePropsContext);
|
||||
|
||||
if (!result || "notFound" in result) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if ("redirect" in result) {
|
||||
redirect(result.redirect.destination);
|
||||
}
|
||||
|
||||
return result.props;
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||
const pageProps = await getPageProps({ params });
|
||||
return <SetupPage {...pageProps} />;
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import CategoryPage from "@pages/apps/categories/[category]";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { notFound } from "next/navigation";
|
||||
import z from "zod";
|
||||
|
||||
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
() => `${APP_NAME} | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const paths = Object.keys(AppCategories);
|
||||
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1`;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Prisma.PrismaClientInitializationError) {
|
||||
// Database is not available at build time. Make sure we fall back to building these pages on demand
|
||||
return [];
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return paths.map((category) => ({ category }));
|
||||
};
|
||||
|
||||
const querySchema = z.object({
|
||||
category: z.nativeEnum(AppCategories),
|
||||
});
|
||||
|
||||
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const p = querySchema.safeParse(params);
|
||||
|
||||
if (!p.success) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const appQuery = await prisma.app.findMany({
|
||||
where: {
|
||||
categories: {
|
||||
has: p.data.category,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
});
|
||||
|
||||
const dbAppsSlugs = appQuery.map((category) => category.slug);
|
||||
|
||||
const appStore = await getAppRegistry();
|
||||
|
||||
const apps = appStore.filter((app) => dbAppsSlugs.includes(app.slug));
|
||||
return {
|
||||
apps,
|
||||
};
|
||||
};
|
||||
|
||||
// @ts-expect-error getData arg
|
||||
export default WithLayout({ getData: getPageProps, Page: CategoryPage })<"P">;
|
||||
export const dynamic = "force-static";
|
|
@ -1,44 +0,0 @@
|
|||
import LegacyPage from "@pages/apps/categories/index";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
() => `Categories | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
const getData = async (ctx: GetServerSidePropsContext) => {
|
||||
const ssr = await ssrInit(ctx);
|
||||
|
||||
const session = await getServerSession({ req: ctx.req });
|
||||
|
||||
let appStore;
|
||||
if (session?.user?.id) {
|
||||
appStore = await getAppRegistryWithCredentials(session.user.id);
|
||||
} else {
|
||||
appStore = await getAppRegistry();
|
||||
}
|
||||
|
||||
const categories = appStore.reduce((c, app) => {
|
||||
for (const category of app.categories) {
|
||||
c[category] = c[category] ? c[category] + 1 : 1;
|
||||
}
|
||||
return c;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
return {
|
||||
categories: Object.entries(categories).map(([name, count]) => ({ name, count })),
|
||||
dehydratedState: ssr.dehydrate(),
|
||||
};
|
||||
};
|
||||
|
||||
export default WithLayout({ getData, Page: LegacyPage, getLayout: null })<"P">;
|
|
@ -1,3 +0,0 @@
|
|||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
export default WithLayout({ getLayout: null })<"L">;
|
|
@ -1,36 +0,0 @@
|
|||
import LegacyPage from "@pages/apps/installed/[category]";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
const querySchema = z.object({
|
||||
category: z.nativeEnum(AppCategories),
|
||||
});
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => `${t("installed_apps")} | ${APP_NAME}`,
|
||||
(t) => t("manage_your_connected_apps")
|
||||
);
|
||||
};
|
||||
|
||||
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const p = querySchema.safeParse(params);
|
||||
|
||||
if (!p.success) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
category: p.data.category,
|
||||
};
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||
await getPageProps({ params });
|
||||
|
||||
return <LegacyPage />;
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
import AppsPage from "@pages/apps";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
||||
import getUserAdminTeams from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import type { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
() => `Apps | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
const getData = async (ctx: GetServerSidePropsContext) => {
|
||||
const ssr = await ssrInit(ctx);
|
||||
|
||||
const session = await getServerSession({ req: ctx.req });
|
||||
|
||||
let appStore, userAdminTeams: UserAdminTeams;
|
||||
if (session?.user?.id) {
|
||||
userAdminTeams = await getUserAdminTeams({ userId: session.user.id, getUserInfo: true });
|
||||
appStore = await getAppRegistryWithCredentials(session.user.id, userAdminTeams);
|
||||
} else {
|
||||
appStore = await getAppRegistry();
|
||||
userAdminTeams = [];
|
||||
}
|
||||
|
||||
const categoryQuery = appStore.map(({ categories }) => ({
|
||||
categories: categories || [],
|
||||
}));
|
||||
|
||||
const categories = categoryQuery.reduce((c, app) => {
|
||||
for (const category of app.categories) {
|
||||
c[category] = c[category] ? c[category] + 1 : 1;
|
||||
}
|
||||
return c;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
return {
|
||||
categories: Object.entries(categories)
|
||||
.map(([name, count]): { name: AppCategories; count: number } => ({
|
||||
name: name as AppCategories,
|
||||
count,
|
||||
}))
|
||||
.sort(function (a, b) {
|
||||
return b.count - a.count;
|
||||
}),
|
||||
appStore,
|
||||
userAdminTeams,
|
||||
dehydratedState: ssr.dehydrate(),
|
||||
};
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout, getData, Page: AppsPage });
|
|
@ -1,10 +0,0 @@
|
|||
import OldPage from "@pages/booking/[uid]";
|
||||
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getData } from "../page";
|
||||
|
||||
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||
|
||||
// @ts-expect-error Type '(context: GetServerSidePropsContext) => Promise<any>' is not assignable to type '(arg: {
|
||||
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: OldPage });
|
|
@ -1,204 +0,0 @@
|
|||
import OldPage from "@pages/booking/[uid]";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { getRecurringBookings, handleSeatsEventTypeOnBooking, getEventTypesFromDB } from "@lib/booking";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
const stringToBoolean = z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => val === "true");
|
||||
|
||||
const querySchema = z.object({
|
||||
uid: z.string(),
|
||||
email: z.string().optional(),
|
||||
eventTypeSlug: z.string().optional(),
|
||||
cancel: stringToBoolean,
|
||||
allRemainingBookings: stringToBoolean,
|
||||
changes: stringToBoolean,
|
||||
reschedule: stringToBoolean,
|
||||
isSuccessBookingPage: stringToBoolean,
|
||||
formerTime: z.string().optional(),
|
||||
seatReferenceUid: z.string().optional(),
|
||||
});
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "",
|
||||
() => ""
|
||||
);
|
||||
|
||||
export const getData = async (context: GetServerSidePropsContext) => {
|
||||
const ssr = await ssrInit(context);
|
||||
const session = await getServerSession(context);
|
||||
let tz: string | null = null;
|
||||
let userTimeFormat: number | null = null;
|
||||
let requiresLoginToUpdate = false;
|
||||
if (session) {
|
||||
const user = await ssr.viewer.me.fetch();
|
||||
tz = user.timeZone;
|
||||
userTimeFormat = user.timeFormat;
|
||||
}
|
||||
|
||||
const parsedQuery = querySchema.safeParse(context.query);
|
||||
|
||||
if (!parsedQuery.success) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { uid, eventTypeSlug, seatReferenceUid } = parsedQuery.data;
|
||||
|
||||
const { uid: maybeUid } = await maybeGetBookingUidFromSeat(prisma, uid);
|
||||
const bookingInfoRaw = await prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: maybeUid,
|
||||
},
|
||||
select: {
|
||||
title: true,
|
||||
id: true,
|
||||
uid: true,
|
||||
description: true,
|
||||
customInputs: true,
|
||||
smsReminderNumber: true,
|
||||
recurringEventId: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
location: true,
|
||||
status: true,
|
||||
metadata: true,
|
||||
cancellationReason: true,
|
||||
responses: true,
|
||||
rejectionReason: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
username: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
eventTypeId: true,
|
||||
eventType: {
|
||||
select: {
|
||||
eventName: true,
|
||||
slug: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
seatsReferences: {
|
||||
select: {
|
||||
referenceUid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!bookingInfoRaw) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const eventTypeRaw = !bookingInfoRaw.eventTypeId
|
||||
? getDefaultEvent(eventTypeSlug || "")
|
||||
: await getEventTypesFromDB(bookingInfoRaw.eventTypeId);
|
||||
if (!eventTypeRaw) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (eventTypeRaw.seatsPerTimeSlot && !seatReferenceUid && !session) {
|
||||
requiresLoginToUpdate = true;
|
||||
}
|
||||
|
||||
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
|
||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||
// probably fixable with json.stringify -> json.parse
|
||||
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
|
||||
bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
|
||||
|
||||
eventTypeRaw.users = !!eventTypeRaw.hosts?.length
|
||||
? eventTypeRaw.hosts.map((host) => host.user)
|
||||
: eventTypeRaw.users;
|
||||
|
||||
if (!eventTypeRaw.users.length) {
|
||||
if (!eventTypeRaw.owner) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
eventTypeRaw.users.push({
|
||||
...eventTypeRaw.owner,
|
||||
});
|
||||
}
|
||||
|
||||
const eventType = {
|
||||
...eventTypeRaw,
|
||||
periodStartDate: eventTypeRaw.periodStartDate?.toString() ?? null,
|
||||
periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
|
||||
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
|
||||
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
||||
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
|
||||
};
|
||||
|
||||
const profile = {
|
||||
name: eventType.team?.name || eventType.users[0]?.name || null,
|
||||
email: eventType.team ? null : eventType.users[0].email || null,
|
||||
theme: (!eventType.team?.name && eventType.users[0]?.theme) || null,
|
||||
brandColor: eventType.team ? null : eventType.users[0].brandColor || null,
|
||||
darkBrandColor: eventType.team ? null : eventType.users[0].darkBrandColor || null,
|
||||
slug: eventType.team?.slug || eventType.users[0]?.username || null,
|
||||
};
|
||||
|
||||
if (bookingInfo !== null && eventType.seatsPerTimeSlot) {
|
||||
await handleSeatsEventTypeOnBooking(eventType, bookingInfo, seatReferenceUid, session?.user.id);
|
||||
}
|
||||
|
||||
const payment = await prisma.payment.findFirst({
|
||||
where: {
|
||||
bookingId: bookingInfo.id,
|
||||
},
|
||||
select: {
|
||||
success: true,
|
||||
refunded: true,
|
||||
currency: true,
|
||||
amount: true,
|
||||
paymentOption: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
|
||||
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
|
||||
profile,
|
||||
eventType,
|
||||
recurringBookings: await getRecurringBookings(bookingInfo.recurringEventId),
|
||||
dehydratedState: ssr.dehydrate(),
|
||||
dynamicEventName: bookingInfo?.eventType?.eventName || "",
|
||||
bookingInfo,
|
||||
paymentStatus: payment,
|
||||
...(tz && { tz }),
|
||||
userTimeFormat,
|
||||
requiresLoginToUpdate,
|
||||
};
|
||||
};
|
||||
|
||||
// @ts-expect-error Argument of type '{ req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
|
||||
export default WithLayout({ getLayout: null, getData, Page: OldPage });
|
|
@ -1,45 +0,0 @@
|
|||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { ssgInit } from "@server/lib/ssg";
|
||||
|
||||
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
|
||||
|
||||
const querySchema = z.object({
|
||||
status: z.enum(validStatuses),
|
||||
});
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => `${APP_NAME} | ${t("bookings")}`,
|
||||
() => ""
|
||||
);
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
return validStatuses.map((status) => ({ status }));
|
||||
};
|
||||
|
||||
const getData = async (ctx: GetServerSidePropsContext) => {
|
||||
const parsedParams = querySchema.safeParse(ctx.params);
|
||||
|
||||
if (!parsedParams.success) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const ssg = await ssgInit(ctx);
|
||||
|
||||
return {
|
||||
status: parsedParams.data.status,
|
||||
dehydratedState: ssg.dehydrate(),
|
||||
};
|
||||
};
|
||||
|
||||
export default WithLayout({ getLayout, getData })<"L">;
|
||||
|
||||
export const dynamic = "force-static";
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user