Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56a2b66190 | ||
|
|
a0f55707e2 | ||
|
|
b247075a19 | ||
|
|
25ec5313fb | ||
|
|
33f35e77fd | ||
|
|
69808bb9a4 | ||
|
|
18b6a93476 | ||
|
|
e627cc51fd | ||
|
|
b349def58b | ||
|
|
6a6faf30ff | ||
|
|
5616dba4db | ||
|
|
bca1a1b86c | ||
|
|
ddf7a234ab | ||
|
|
fa7260d72a | ||
|
|
5f9f6ef025 | ||
|
|
3d41c64d81 | ||
|
|
8a9b985760 | ||
|
|
c8956680ad | ||
|
|
c2668acd33 | ||
|
|
89bf8321ba | ||
|
|
da625f1527 | ||
|
|
2dddd4ce77 | ||
|
|
cc1d606ba8 | ||
|
|
262c8cf37c | ||
|
|
4366405b21 | ||
|
|
07399974ce | ||
|
|
9d7cd73c7f | ||
|
|
1551a293c5 | ||
|
|
47e948fbbc | ||
|
|
c8b01f6992 | ||
|
|
f3143d99ee | ||
|
|
61a5c3a3b5 | ||
|
|
056edd8ec2 | ||
|
|
d8387835a8 | ||
|
|
26ea249481 | ||
|
|
2d26d73eb3 | ||
|
|
b8b24c04dc | ||
|
|
62dcd6612c | ||
|
|
295748adca | ||
|
|
6e93224c9b | ||
|
|
4d8198d113 | ||
|
|
5fe0ca7913 | ||
|
|
cfb625e934 | ||
|
|
e12b21a73c | ||
|
|
841346c5f2 | ||
|
|
1270742078 | ||
|
|
517cfde5b8 | ||
|
|
51bf613621 | ||
|
|
f4c3ab6431 | ||
|
|
e8acc3e500 | ||
|
|
a6ba0f5b57 | ||
|
|
b82d924f03 | ||
|
|
1cdb51f424 | ||
|
|
aca0dc1b01 | ||
|
|
e5e8c33c34 | ||
|
|
c76847a8f1 | ||
|
|
7b1ced914b | ||
|
|
a6cd173910 | ||
|
|
dcd39c3f04 | ||
|
|
aa0a1fcc95 | ||
|
|
76cf352bce | ||
|
|
86ab19be30 | ||
|
|
e4b766b9d6 | ||
|
|
2529f71770 | ||
|
|
9ea0f02c93 | ||
|
|
fb1828c8f9 | ||
|
|
12f2c1f431 | ||
|
|
29e9bd0ee7 | ||
|
|
9c794bac1a | ||
|
|
feca7c23fb | ||
|
|
31599cdd26 | ||
|
|
e018eb9ebe | ||
|
|
a3f3f1f263 | ||
|
|
d19eaae5b6 | ||
|
|
4eecd42204 | ||
|
|
b9f90144c5 | ||
|
|
8b07b839e9 | ||
|
|
2a98ca4b91 | ||
|
|
b9064b1f72 | ||
|
|
0ec71e52ef | ||
|
|
c20835a4c8 | ||
|
|
8c63b42cec | ||
|
|
ddb752102f | ||
|
|
ef0a7950cf | ||
|
|
641301afab | ||
|
|
7d1bc5f0c5 | ||
|
|
aaccfa49ce | ||
|
|
1b1f3ddfd9 | ||
|
|
f7eda5aa3a | ||
|
|
b2b846647d | ||
|
|
4edc973876 | ||
|
|
3b0126ff35 | ||
|
|
cc4192817b | ||
|
|
a8a10c0f35 | ||
|
|
5f7d904349 | ||
|
|
abce20824f | ||
|
|
76a8c89d33 | ||
|
|
fc9360d99a | ||
|
|
a5c21d5e3e | ||
|
|
3960c8a954 | ||
|
|
0cbcda7430 | ||
|
|
ff12e35748 | ||
|
|
15d967b1d2 | ||
|
|
b309be1d94 | ||
|
|
94dc2643f2 | ||
|
|
6440d3da6a | ||
|
|
9e08cf88f4 | ||
|
|
8b3eb986df | ||
|
|
8a6a7524cc | ||
|
|
1dbdb8a4d4 | ||
|
|
4fb0852505 | ||
|
|
0815ab715b | ||
|
|
944ff68463 | ||
|
|
676254e96b | ||
|
|
207bcb9a97 | ||
|
|
4d825672db | ||
|
|
ba86e7f540 | ||
|
|
3206e4ea12 | ||
|
|
dbcebec31b | ||
|
|
f427c09e88 | ||
|
|
ded838881f | ||
|
|
08da9d805a | ||
|
|
7cd0d5c3c3 | ||
|
|
b018ee34a7 | ||
|
|
17345f9326 | ||
|
|
a1d7b30d51 | ||
|
|
87dbfba5df | ||
|
|
e5d44d029a |
No files matched your search
+12
-11
@@ -17,16 +17,17 @@ scripts/ @calcom/core
|
|||||||
apps/docs @calcom/docs
|
apps/docs @calcom/docs
|
||||||
apps/swagger @calcom/docs
|
apps/swagger @calcom/docs
|
||||||
|
|
||||||
packages/app-store-cli @hariombalhara
|
packages/app-store-cli @hariombalhara @calcom/core
|
||||||
packages/dayjs @zomars
|
packages/dayjs @zomars @calcom/core
|
||||||
packages/emails @zomars
|
packages/emails @zomars @calcom/core
|
||||||
packages/embeds @hariombalhara
|
packages/embeds @hariombalhara @calcom/core
|
||||||
packages/eslint-plugin @zomars
|
packages/eslint-plugin @zomars @calcom/core
|
||||||
packages/features @zomars
|
packages/features @zomars @calcom/core
|
||||||
packages/features/kbar @alishaz-polymath
|
packages/features/kbar @alishaz-polymath @calcom/core
|
||||||
packages/features/ee/impersonation @sean-brydon
|
packages/features/ee/impersonation @sean-brydon @calcom/core
|
||||||
packages/features/ee/payments @alannnc
|
packages/features/ee/payments @alannnc @calcom/core
|
||||||
packages/features/ee/workflows @CarinaWolli
|
packages/features/ee/workflows @CarinaWolli @calcom/core
|
||||||
packages/features/tips @PeerRich
|
packages/features/tips @PeerRich @calcom/core
|
||||||
packages/ui @calcom/ui
|
packages/ui @calcom/ui
|
||||||
packages/config @calcom/core
|
packages/config @calcom/core
|
||||||
|
packages/app-store/ee/routing-forms @hariombalhara @calcom/core
|
||||||
@@ -1,23 +1,28 @@
|
|||||||
name: Cache production build binaries
|
name: Cache production build binaries
|
||||||
description: "Cache or restore if necessary"
|
description: "Cache or restore if necessary"
|
||||||
env:
|
inputs:
|
||||||
node_version: v16.x
|
node_version:
|
||||||
|
required: false
|
||||||
|
default: v16.x
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Cache production build
|
- name: Cache production build
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
|
id: cache-build
|
||||||
env:
|
env:
|
||||||
cache-name: prod-build
|
cache-name: prod-build
|
||||||
key-1: ${{ env.node_version }}-${{ hashFiles('yarn.lock') }}
|
key-1: ${{ inputs.node_version }}-${{ hashFiles('yarn.lock') }}
|
||||||
key-2: ${{ hashFiles('apps/web/next.config.js') }}
|
key-2: ${{ hashFiles('apps/**/**.[jt]s', 'apps/**/**.[jt]sx', 'packages/**/**.[jt]s', 'packages/**/**.[jt]sx', '!**/node_modules') }}
|
||||||
|
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||||
|
# Ensures production-build.yml will always be fresh
|
||||||
|
key-4: ${{ github.sha }}
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
apps/web/.next
|
${{ github.workspace }}/apps/web/.next
|
||||||
node_modules/.cache/turbo
|
|
||||||
**/.turbo/**
|
**/.turbo/**
|
||||||
**/dist/**
|
**/dist/**
|
||||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ github.sha }}
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||||
restore-keys: |
|
- run: yarn build
|
||||||
${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-
|
shell: bash
|
||||||
@@ -2,7 +2,8 @@ name: Cache database between jobs
|
|||||||
description: "Cache or restore if necessary"
|
description: "Cache or restore if necessary"
|
||||||
inputs:
|
inputs:
|
||||||
DATABASE_URL:
|
DATABASE_URL:
|
||||||
required: true
|
required: false
|
||||||
|
default: "postgresql://postgres:@localhost:5432/calendso"
|
||||||
path:
|
path:
|
||||||
required: false
|
required: false
|
||||||
default: "backups/backup.sql"
|
default: "backups/backup.sql"
|
||||||
@@ -14,11 +15,14 @@ runs:
|
|||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: cache-db
|
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 }}
|
||||||
with:
|
with:
|
||||||
path: ${{ inputs.path }}
|
path: ${{ inputs.path }}
|
||||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ github.sha }}
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}
|
||||||
restore-keys: |
|
- run: yarn db-seed
|
||||||
${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-
|
if: steps.cache-db.outputs.cache-hit != 'true'
|
||||||
|
shell: bash
|
||||||
- name: Postgres Dump Backup
|
- name: Postgres Dump Backup
|
||||||
if: steps.cache-db.outputs.cache-hit != 'true'
|
if: steps.cache-db.outputs.cache-hit != 'true'
|
||||||
uses: tj-actions/pg-dump@v2.3
|
uses: tj-actions/pg-dump@v2.3
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
name: "Set environment variables"
|
||||||
|
description: "Configures environment variables for a workflow"
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Create env file
|
||||||
|
uses: actions/cache@v3
|
||||||
|
id: env-cache
|
||||||
|
with:
|
||||||
|
path: gh.env
|
||||||
|
key: env-cache-${{ hashFiles('gh.env') }}
|
||||||
|
restore-keys: env-cache-
|
||||||
|
- name: Set Environment Variables
|
||||||
|
uses: tw3lveparsecs/github-actions-setvars@latest
|
||||||
|
with:
|
||||||
|
envFilePath: gh.env
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
name: Production Build
|
|
||||||
description: "Creates a production build, caches it and restores if necessary"
|
|
||||||
inputs:
|
|
||||||
DATABASE_URL:
|
|
||||||
required: true
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL:
|
|
||||||
required: true
|
|
||||||
NEXT_PUBLIC_WEBSITE_URL:
|
|
||||||
required: true
|
|
||||||
NEXTAUTH_SECRET:
|
|
||||||
required: true
|
|
||||||
GOOGLE_API_CREDENTIALS:
|
|
||||||
required: true
|
|
||||||
GOOGLE_LOGIN_ENABLED:
|
|
||||||
required: true
|
|
||||||
CRON_API_KEY:
|
|
||||||
required: false
|
|
||||||
CALENDSO_ENCRYPTION_KEY:
|
|
||||||
required: true
|
|
||||||
DAILY_API_KEY:
|
|
||||||
required: true
|
|
||||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY:
|
|
||||||
required: true
|
|
||||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE:
|
|
||||||
required: true
|
|
||||||
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE:
|
|
||||||
required: true
|
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE:
|
|
||||||
required: true
|
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE:
|
|
||||||
required: true
|
|
||||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN:
|
|
||||||
required: true
|
|
||||||
STRIPE_PRIVATE_KEY:
|
|
||||||
required: true
|
|
||||||
STRIPE_CLIENT_ID:
|
|
||||||
required: true
|
|
||||||
STRIPE_WEBHOOK_SECRET:
|
|
||||||
required: true
|
|
||||||
STRIPE_PRO_PLAN_PRODUCT_ID:
|
|
||||||
required: true
|
|
||||||
STRIPE_PREMIUM_PLAN_PRODUCT_ID:
|
|
||||||
required: true
|
|
||||||
STRIPE_FREE_PLAN_PRODUCT_ID:
|
|
||||||
required: true
|
|
||||||
PAYMENT_FEE_PERCENTAGE:
|
|
||||||
required: true
|
|
||||||
PAYMENT_FEE_FIXED:
|
|
||||||
required: true
|
|
||||||
SAML_DATABASE_URL:
|
|
||||||
required: true
|
|
||||||
SAML_ADMINS:
|
|
||||||
required: true
|
|
||||||
NEXTAUTH_URL:
|
|
||||||
required: true
|
|
||||||
NEXT_PUBLIC_IS_E2E:
|
|
||||||
required: true
|
|
||||||
TURBO_TOKEN:
|
|
||||||
required: true
|
|
||||||
TURBO_TEAM:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: "composite"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: ./.github/actions/yarn-install
|
|
||||||
- uses: ./.github/actions/cache-build
|
|
||||||
id: prod-build-cache
|
|
||||||
- name: Next.js production build
|
|
||||||
if: steps.prod-build-cache.outputs.cache-hit != 'true'
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
DATABASE_URL: ${{ inputs.DATABASE_URL }}
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL: ${{ inputs.NEXT_PUBLIC_WEBAPP_URL }}
|
|
||||||
NEXT_PUBLIC_WEBSITE_URL: ${{ inputs.NEXT_PUBLIC_WEBSITE_URL }}
|
|
||||||
NEXTAUTH_SECRET: ${{ inputs.NEXTAUTH_SECRET }}
|
|
||||||
GOOGLE_API_CREDENTIALS: ${{ inputs.GOOGLE_API_CREDENTIALS }}
|
|
||||||
GOOGLE_LOGIN_ENABLED: ${{ inputs.GOOGLE_LOGIN_ENABLED }}
|
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ inputs.CALENDSO_ENCRYPTION_KEY }}
|
|
||||||
DAILY_API_KEY: ${{ inputs.DAILY_API_KEY }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ inputs.NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
|
||||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ inputs.NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ inputs.NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ inputs.NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE: ${{ inputs.NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN: ${{ inputs.NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN }}
|
|
||||||
STRIPE_PRIVATE_KEY: ${{ inputs.STRIPE_PRIVATE_KEY }}
|
|
||||||
STRIPE_CLIENT_ID: ${{ inputs.STRIPE_CLIENT_ID }}
|
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ inputs.STRIPE_WEBHOOK_SECRET }}
|
|
||||||
STRIPE_PRO_PLAN_PRODUCT_ID: ${{ inputs.STRIPE_PRO_PLAN_PRODUCT_ID }}
|
|
||||||
STRIPE_PREMIUM_PLAN_PRODUCT_ID: ${{ inputs.STRIPE_PREMIUM_PLAN_PRODUCT_ID }}
|
|
||||||
STRIPE_FREE_PLAN_PRODUCT_ID: ${{ inputs.STRIPE_FREE_PLAN_PRODUCT_ID }}
|
|
||||||
PAYMENT_FEE_PERCENTAGE: ${{ inputs.PAYMENT_FEE_PERCENTAGE }}
|
|
||||||
PAYMENT_FEE_FIXED: ${{ inputs.PAYMENT_FEE_FIXED }}
|
|
||||||
SAML_DATABASE_URL: ${{ inputs.SAML_DATABASE_URL }}
|
|
||||||
SAML_ADMINS: ${{ inputs.SAML_ADMINS }}
|
|
||||||
NEXTAUTH_URL: ${{ inputs.NEXTAUTH_URL }}
|
|
||||||
NEXT_PUBLIC_IS_E2E: ${{ inputs.NEXT_PUBLIC_IS_E2E }}
|
|
||||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
|
||||||
run: |
|
|
||||||
yarn db-seed
|
|
||||||
yarn build
|
|
||||||
@@ -1,30 +1,19 @@
|
|||||||
name: Yarn install
|
name: Yarn install
|
||||||
description: "Install all NPM dependencies, caches them and restores if necessary"
|
description: "Install all NPM dependencies, caches them and restores if necessary"
|
||||||
env:
|
inputs:
|
||||||
node_version: v16.x
|
node_version:
|
||||||
|
required: false
|
||||||
|
default: v16.x
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Use Node ${{ env.node_version }}
|
- name: Use Node ${{ inputs.node_version }}
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.node_version }}
|
node-version: ${{ inputs.node_version }}
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
- name: node_modules cache
|
|
||||||
id: node-modules-cache
|
|
||||||
uses: actions/cache@v3
|
|
||||||
env:
|
|
||||||
cache-name: node-modules-yarn
|
|
||||||
cache-key: ${{ env.node_version }}-${{ hashFiles('yarn.lock', 'packages/prisma/schema.prisma') }}
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
**/node_modules/**
|
|
||||||
packages/prisma/zod
|
|
||||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.cache-key }}
|
|
||||||
restore-keys: ${{ runner.os }}-${{ env.cache-name }}-
|
|
||||||
- name: Yarn install
|
- name: Yarn install
|
||||||
shell: bash
|
shell: bash
|
||||||
if: steps.node-modules-cache.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
run: |
|
||||||
yarn install --prefer-offline --frozen-lockfile
|
yarn install --prefer-offline --frozen-lockfile
|
||||||
yarn prisma generate
|
yarn prisma generate
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: cleanup caches by a branch
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- closed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cleanup:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
run: |
|
||||||
|
gh extension install actions/gh-actions-cache
|
||||||
|
|
||||||
|
REPO=${{ github.repository }}
|
||||||
|
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
|
||||||
|
|
||||||
|
echo "Fetching list of cache key"
|
||||||
|
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
|
||||||
|
|
||||||
|
## Setting this to not fail the workflow while deleting cache keys.
|
||||||
|
set +e
|
||||||
|
echo "Deleting caches..."
|
||||||
|
for cacheKey in $cacheKeysForPR
|
||||||
|
do
|
||||||
|
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
|
||||||
|
done
|
||||||
|
echo "Done"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -7,37 +7,6 @@ jobs:
|
|||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
name: E2E App-Store Apps
|
name: E2E App-Store Apps
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
env:
|
|
||||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
|
||||||
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
|
|
||||||
NEXTAUTH_SECRET: secret
|
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
|
||||||
GOOGLE_LOGIN_ENABLED: true
|
|
||||||
# CRON_API_KEY: xxx
|
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
|
||||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
|
||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
|
||||||
PAYMENT_FEE_PERCENTAGE: 0.005
|
|
||||||
PAYMENT_FEE_FIXED: 10
|
|
||||||
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
|
||||||
SAML_ADMINS: pro@example.com
|
|
||||||
NEXTAUTH_URL: http://localhost:3000/api/auth
|
|
||||||
NEXT_PUBLIC_IS_E2E: 1
|
|
||||||
# EMAIL_FROM: e2e@cal.com
|
|
||||||
# EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
|
||||||
# EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
|
||||||
# EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
|
||||||
# EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD }}
|
|
||||||
# MS_GRAPH_CLIENT_ID: xxx
|
|
||||||
# MS_GRAPH_CLIENT_SECRET: xxx
|
|
||||||
# ZOOM_CLIENT_ID: xxx
|
|
||||||
# ZOOM_CLIENT_SECRET: xxx
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:12.1
|
image: postgres:12.1
|
||||||
@@ -53,13 +22,13 @@ jobs:
|
|||||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/yarn-playwright-install
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
|
- uses: ./.github/actions/env-read-file
|
||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
with:
|
|
||||||
DATABASE_URL: ${{ env.DATABASE_URL }}
|
|
||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn test-e2e:app-store
|
run: yarn test-e2e:app-store
|
||||||
|
env:
|
||||||
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
|
|||||||
@@ -6,41 +6,6 @@ jobs:
|
|||||||
e2e-embed:
|
e2e-embed:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
|
||||||
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
|
|
||||||
NEXTAUTH_SECRET: secret
|
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
|
||||||
GOOGLE_LOGIN_ENABLED: true
|
|
||||||
# CRON_API_KEY: xxx
|
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
|
||||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE: ${{ secrets.NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN: 1
|
|
||||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
|
||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
|
||||||
PAYMENT_FEE_PERCENTAGE: 0.005
|
|
||||||
PAYMENT_FEE_FIXED: 10
|
|
||||||
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
|
||||||
SAML_ADMINS: pro@example.com
|
|
||||||
NEXTAUTH_URL: http://localhost:3000/api/auth
|
|
||||||
NEXT_PUBLIC_IS_E2E: 1
|
|
||||||
# EMAIL_FROM: e2e@cal.com
|
|
||||||
# EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
|
||||||
# EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
|
||||||
# EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
|
||||||
# EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD }}
|
|
||||||
# MS_GRAPH_CLIENT_ID: xxx
|
|
||||||
# MS_GRAPH_CLIENT_SECRET: xxx
|
|
||||||
# ZOOM_CLIENT_ID: xxx
|
|
||||||
# ZOOM_CLIENT_SECRET: xxx
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:12.1
|
image: postgres:12.1
|
||||||
@@ -56,22 +21,23 @@ jobs:
|
|||||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/yarn-playwright-install
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
|
- uses: ./.github/actions/env-read-file
|
||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
with:
|
|
||||||
DATABASE_URL: ${{ env.DATABASE_URL }}
|
|
||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn turbo run embed-tests-update-snapshots:ci --scope=@calcom/embed-react --concurrency=1
|
run: yarn turbo run embed-tests-update-snapshots:ci --scope=@calcom/embed-react --concurrency=1
|
||||||
|
env:
|
||||||
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
- name: Upload embed-core results
|
- name: Upload embed-core results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: test-results-core
|
name: test-results-embed-core
|
||||||
path: packages/embeds/embed-core/playwright/results
|
path: packages/embeds/embed-core/playwright/results
|
||||||
|
|
||||||
- name: Upload embed-react results
|
- name: Upload embed-react results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: test-results-react
|
name: test-results-embed-react
|
||||||
path: packages/embeds/embed-react/playwright/results
|
path: packages/embeds/embed-react/playwright/results
|
||||||
@@ -4,46 +4,8 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
e2e:
|
e2e:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
name: E2E tests
|
||||||
env:
|
runs-on: ubuntu-latest
|
||||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
|
||||||
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
|
|
||||||
NEXTAUTH_SECRET: secret
|
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
|
||||||
GOOGLE_LOGIN_ENABLED: true
|
|
||||||
# CRON_API_KEY: xxx
|
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
|
||||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
|
||||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE: ${{ secrets.NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN: 1
|
|
||||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
|
||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
|
||||||
STRIPE_PRO_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_PRO_PLAN_PRODUCT_ID }}
|
|
||||||
STRIPE_PREMIUM_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_PREMIUM_PLAN_PRODUCT_ID }}
|
|
||||||
STRIPE_FREE_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_FREE_PLAN_PRODUCT_ID }}
|
|
||||||
PAYMENT_FEE_PERCENTAGE: 0.005
|
|
||||||
PAYMENT_FEE_FIXED: 10
|
|
||||||
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
|
||||||
SAML_ADMINS: pro@example.com
|
|
||||||
NEXTAUTH_URL: http://localhost:3000/api/auth
|
|
||||||
NEXT_PUBLIC_IS_E2E: 1
|
|
||||||
# EMAIL_FROM: e2e@cal.com
|
|
||||||
# EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
|
||||||
# EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
|
||||||
# EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
|
||||||
# EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD }}
|
|
||||||
# MS_GRAPH_CLIENT_ID: xxx
|
|
||||||
# MS_GRAPH_CLIENT_SECRET: xxx
|
|
||||||
# ZOOM_CLIENT_ID: xxx
|
|
||||||
# ZOOM_CLIENT_SECRET: xxx
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:12.1
|
image: postgres:12.1
|
||||||
@@ -52,19 +14,20 @@ jobs:
|
|||||||
POSTGRES_DB: calendso
|
POSTGRES_DB: calendso
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/yarn-playwright-install
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
|
- uses: ./.github/actions/env-read-file
|
||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
with:
|
|
||||||
DATABASE_URL: ${{ env.DATABASE_URL }}
|
|
||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
- run: yarn test-e2e
|
- name: Run Tests
|
||||||
- name: Upload test results
|
run: yarn test-e2e
|
||||||
|
env:
|
||||||
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: Create .env file
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
env:
|
||||||
|
INPUT_ENV_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||||
|
INPUT_ENV_NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
||||||
|
INPUT_ENV_NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
|
||||||
|
INPUT_ENV_NEXTAUTH_SECRET: secret
|
||||||
|
INPUT_ENV_GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||||
|
INPUT_ENV_GOOGLE_LOGIN_ENABLED: true
|
||||||
|
# INPUT_ENV_CRON_API_KEY: xxx
|
||||||
|
INPUT_ENV_CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
|
INPUT_ENV_NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||||
|
INPUT_ENV_STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||||
|
INPUT_ENV_STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||||
|
INPUT_ENV_STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||||
|
INPUT_ENV_PAYMENT_FEE_PERCENTAGE: 0.005
|
||||||
|
INPUT_ENV_PAYMENT_FEE_FIXED: 10
|
||||||
|
INPUT_ENV_SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||||
|
INPUT_ENV_SAML_ADMINS: pro@example.com
|
||||||
|
INPUT_ENV_NEXTAUTH_URL: http://localhost:3000/api/auth
|
||||||
|
INPUT_ENV_NEXT_PUBLIC_IS_E2E: 1
|
||||||
|
# INPUT_ENV_EMAIL_FROM: e2e@cal.com
|
||||||
|
# INPUT_ENV_EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
||||||
|
# INPUT_ENV_EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
||||||
|
# INPUT_ENV_EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
||||||
|
# INPUT_ENV_EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD }}
|
||||||
|
# INPUT_ENV_MS_GRAPH_CLIENT_ID: xxx
|
||||||
|
# INPUT_ENV_MS_GRAPH_CLIENT_SECRET: xxx
|
||||||
|
# INPUT_ENV_ZOOM_CLIENT_ID: xxx
|
||||||
|
# INPUT_ENV_ZOOM_CLIENT_SECRET: xxx
|
||||||
|
INPUT_ENV_TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
INPUT_ENV_TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create_env_file:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ozaytsev86/create-env-file@v1
|
||||||
|
with:
|
||||||
|
file-name: ${{ github.workspace }}/gh.env
|
||||||
|
- uses: actions/cache@v3
|
||||||
|
id: env-cache
|
||||||
|
with:
|
||||||
|
path: gh.env
|
||||||
|
key: env-cache-${{ hashFiles('gh.env') }}
|
||||||
@@ -3,7 +3,7 @@ on:
|
|||||||
workflow_call:
|
workflow_call:
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Production build
|
name: Production build
|
||||||
|
if: ${{ github.event_name == 'push' }}
|
||||||
uses: ./.github/workflows/production-build.yml
|
uses: ./.github/workflows/production-build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
analyze:
|
analyze:
|
||||||
needs: build
|
needs: build
|
||||||
|
if: always()
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ on:
|
|||||||
pull_request_target:
|
pull_request_target:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- "**.md"
|
||||||
|
- ".github/CODEOWNERS"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -32,38 +35,35 @@ jobs:
|
|||||||
- 'apps/web/**'
|
- 'apps/web/**'
|
||||||
- 'packages/embeds/**'
|
- 'packages/embeds/**'
|
||||||
|
|
||||||
setup:
|
env:
|
||||||
name: Yarn install & cache
|
name: Create env file
|
||||||
uses: ./.github/workflows/yarn-install.yml
|
uses: ./.github/workflows/env-create-file.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
type-check:
|
type-check:
|
||||||
name: Type check
|
name: Type check
|
||||||
needs: setup
|
|
||||||
uses: ./.github/workflows/check-types.yml
|
uses: ./.github/workflows/check-types.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: Unit tests
|
name: Unit tests
|
||||||
needs: setup
|
|
||||||
uses: ./.github/workflows/test.yml
|
uses: ./.github/workflows/test.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Linters
|
name: Linters
|
||||||
needs: setup
|
|
||||||
uses: ./.github/workflows/lint.yml
|
uses: ./.github/workflows/lint.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Production build
|
name: Production build
|
||||||
needs: setup
|
needs: env
|
||||||
uses: ./.github/workflows/production-build.yml
|
uses: ./.github/workflows/production-build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
name: E2E tests
|
name: E2E tests
|
||||||
needs: [lint, build]
|
needs: [changes, lint, build]
|
||||||
uses: ./.github/workflows/e2e.yml
|
uses: ./.github/workflows/e2e.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
@@ -78,10 +78,19 @@ jobs:
|
|||||||
name: E2E embeds tests
|
name: E2E embeds tests
|
||||||
if: ${{ needs.changes.outputs.embed == 'true' }}
|
if: ${{ needs.changes.outputs.embed == 'true' }}
|
||||||
needs: [changes, lint, build]
|
needs: [changes, lint, build]
|
||||||
uses: ./.github/workflows/e2e.yml
|
uses: ./.github/workflows/e2e-embed.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
analyze:
|
analyze:
|
||||||
needs: build
|
needs: build
|
||||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
required:
|
||||||
|
needs: [lint, type-check, test, build, e2e]
|
||||||
|
if: always()
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: fail if conditional jobs failed
|
||||||
|
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||||
|
run: exit 1
|
||||||
@@ -3,36 +3,6 @@ name: Production Build
|
|||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
||||||
env:
|
|
||||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
|
||||||
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
|
|
||||||
NEXTAUTH_SECRET: secret
|
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
|
||||||
GOOGLE_LOGIN_ENABLED: true
|
|
||||||
# CRON_API_KEY: xxx
|
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
|
||||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
|
||||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE: ${{ secrets.NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN: 1
|
|
||||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
|
||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
|
||||||
STRIPE_PRO_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_PRO_PLAN_PRODUCT_ID }}
|
|
||||||
STRIPE_PREMIUM_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_PREMIUM_PLAN_PRODUCT_ID }}
|
|
||||||
STRIPE_FREE_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_FREE_PLAN_PRODUCT_ID }}
|
|
||||||
PAYMENT_FEE_PERCENTAGE: 0.005
|
|
||||||
PAYMENT_FEE_FIXED: 10
|
|
||||||
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
|
||||||
SAML_ADMINS: pro@example.com
|
|
||||||
NEXTAUTH_URL: http://localhost:3000/api/auth
|
|
||||||
NEXT_PUBLIC_IS_E2E: 1
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
@@ -48,38 +18,8 @@ jobs:
|
|||||||
- 5432:5432
|
- 5432:5432
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/yarn-build
|
- uses: ./.github/actions/env-read-file
|
||||||
with:
|
|
||||||
# GitHub Composite actions cannot read secrets, we have to pass them in here
|
|
||||||
DATABASE_URL: ${{ env.DATABASE_URL }}
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL: ${{ env.NEXT_PUBLIC_WEBAPP_URL }}
|
|
||||||
NEXT_PUBLIC_WEBSITE_URL: ${{ env.NEXT_PUBLIC_WEBSITE_URL }}
|
|
||||||
NEXTAUTH_SECRET: ${{ env.NEXTAUTH_SECRET }}
|
|
||||||
GOOGLE_API_CREDENTIALS: ${{ env.GOOGLE_API_CREDENTIALS }}
|
|
||||||
GOOGLE_LOGIN_ENABLED: ${{ env.GOOGLE_LOGIN_ENABLED }}
|
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ env.CALENDSO_ENCRYPTION_KEY }}
|
|
||||||
DAILY_API_KEY: ${{ env.DAILY_API_KEY }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
|
||||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ env.NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ env.NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ env.NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE: ${{ env.NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE }}
|
|
||||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN: ${{ env.NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN }}
|
|
||||||
STRIPE_PRIVATE_KEY: ${{ env.STRIPE_PRIVATE_KEY }}
|
|
||||||
STRIPE_CLIENT_ID: ${{ env.STRIPE_CLIENT_ID }}
|
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ env.STRIPE_WEBHOOK_SECRET }}
|
|
||||||
STRIPE_PRO_PLAN_PRODUCT_ID: ${{ env.STRIPE_PRO_PLAN_PRODUCT_ID }}
|
|
||||||
STRIPE_PREMIUM_PLAN_PRODUCT_ID: ${{ env.STRIPE_PREMIUM_PLAN_PRODUCT_ID }}
|
|
||||||
STRIPE_FREE_PLAN_PRODUCT_ID: ${{ env.STRIPE_FREE_PLAN_PRODUCT_ID }}
|
|
||||||
PAYMENT_FEE_PERCENTAGE: ${{ env.PAYMENT_FEE_PERCENTAGE }}
|
|
||||||
PAYMENT_FEE_FIXED: ${{ env.PAYMENT_FEE_FIXED }}
|
|
||||||
SAML_DATABASE_URL: ${{ env.SAML_DATABASE_URL }}
|
|
||||||
SAML_ADMINS: ${{ env.SAML_ADMINS }}
|
|
||||||
NEXTAUTH_URL: ${{ env.NEXTAUTH_URL }}
|
|
||||||
NEXT_PUBLIC_IS_E2E: ${{ env.NEXT_PUBLIC_IS_E2E }}
|
|
||||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
|
||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
with:
|
- uses: ./.github/actions/cache-build
|
||||||
DATABASE_URL: ${{ env.DATABASE_URL }}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
name: TODO to Issue
|
name: TODO to Issue
|
||||||
on:
|
on:
|
||||||
pull_request_target: # So we can test on forks
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -12,17 +12,13 @@ on:
|
|||||||
description: "By default, the commit entered above is compared to the one directly before it; to go back further, enter an earlier SHA here"
|
description: "By default, the commit entered above is compared to the one directly before it; to go back further, enter an earlier SHA here"
|
||||||
required: false
|
required: false
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
jobs:
|
jobs:
|
||||||
todos:
|
todos:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
fetch-depth: 2
|
|
||||||
- name: "Add TODOs to issues"
|
- name: "Add TODOs to issues"
|
||||||
uses: "alstr/todo-to-issue-action@v4"
|
uses: "alstr/todo-to-issue-action@v4"
|
||||||
env:
|
env:
|
||||||
|
|||||||
Vendored
+11
-29
@@ -2,37 +2,19 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Next.js: Server",
|
"type": "node",
|
||||||
"type": "node-terminal",
|
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "npm run dev",
|
"name": "Next.js Node Debug",
|
||||||
"skipFiles": ["<node_internals>/**"],
|
"runtimeExecutable": "${workspaceFolder}/node_modules/next/dist/bin/next",
|
||||||
"outFiles": [
|
"env": {
|
||||||
"${workspaceFolder}/**/*.js",
|
"NODE_OPTIONS": "--inspect"
|
||||||
"!**/node_modules/**"
|
},
|
||||||
],
|
"cwd": "${workspaceFolder}/apps/web",
|
||||||
"sourceMaps": true,
|
|
||||||
"resolveSourceMapLocations": [
|
|
||||||
"${workspaceFolder}/**",
|
|
||||||
"!**/node_modules/**"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Next.js: Client",
|
|
||||||
"type": "pwa-chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"url": "http://localhost:3000"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Next.js: Full Stack",
|
|
||||||
"type": "node-terminal",
|
|
||||||
"request": "launch",
|
|
||||||
"command": "npm run dev",
|
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"serverReadyAction": {
|
"sourceMapPathOverrides": {
|
||||||
"pattern": "started server on .+, url: (https?://.+)",
|
"meteor://💻app/*": "${workspaceFolder}/*",
|
||||||
"uriFormat": "%s",
|
"webpack:///./~/*": "${workspaceFolder}/node_modules/*",
|
||||||
"action": "debugWithChrome"
|
"webpack://?:*/*": "${workspaceFolder}/*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -128,6 +128,6 @@ If you get errors, be sure to fix them before committing.
|
|||||||
## Making a Pull Request
|
## Making a Pull Request
|
||||||
|
|
||||||
- Be sure to [check the "Allow edits from maintainers" option](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) while creating you PR.
|
- Be sure to [check the "Allow edits from maintainers" option](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) while creating you PR.
|
||||||
- If your PR refers to or fixes an issue, be sure to add `refs #XXX` or `fixes #XXX` to the PR description. Replacing `XXX` with the respective issue number. Se more about [Linking a pull request to an issue
|
- If your PR refers to or fixes an issue, be sure to add `refs #XXX` or `fixes #XXX` to the PR description. Replacing `XXX` with the respective issue number. See more about [Linking a pull request to an issue
|
||||||
](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
|
](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
|
||||||
- Be sure to fill the PR Template accordingly.
|
- Be sure to fill the PR Template accordingly.
|
||||||
@@ -352,6 +352,15 @@ We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:is
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- CONTRIBUTORS -->
|
||||||
|
|
||||||
|
### Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/calcom/cal.com/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=calcom/cal.com" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
<!-- TRANSLATIONS -->
|
<!-- TRANSLATIONS -->
|
||||||
|
|
||||||
### Translations
|
### Translations
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"iframe-resizer-react": "^1.1.0",
|
"iframe-resizer-react": "^1.1.0",
|
||||||
"next": "^13.1.1",
|
"next": "^13.2.1",
|
||||||
"nextra": "^1.1.0",
|
"nextra": "^1.1.0",
|
||||||
"nextra-theme-docs": "^1.2.2",
|
"nextra-theme-docs": "^1.2.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
@@ -1,21 +1,7 @@
|
|||||||
const withBundleAnalyzer = require("@next/bundle-analyzer");
|
const withBundleAnalyzer = require("@next/bundle-analyzer");
|
||||||
|
|
||||||
const withTM = require("next-transpile-modules")([
|
|
||||||
"@calcom/app-store",
|
|
||||||
"@calcom/dayjs",
|
|
||||||
"@calcom/emails",
|
|
||||||
"@calcom/trpc",
|
|
||||||
"@calcom/embed-core",
|
|
||||||
"@calcom/embed-react",
|
|
||||||
"@calcom/features",
|
|
||||||
"@calcom/lib",
|
|
||||||
"@calcom/prisma",
|
|
||||||
"@calcom/ui",
|
|
||||||
]);
|
|
||||||
const glob = require("glob");
|
|
||||||
|
|
||||||
const plugins = [];
|
const plugins = [];
|
||||||
plugins.push(withTM, withBundleAnalyzer({ enabled: process.env.ANALYZE === "true" }));
|
plugins.push(withBundleAnalyzer({ enabled: process.env.ANALYZE === "true" }));
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
@@ -27,7 +13,18 @@ const nextConfig = {
|
|||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
experimental: { images: { allowFutureImage: true } },
|
transpilePackages: [
|
||||||
|
"@calcom/app-store",
|
||||||
|
"@calcom/dayjs",
|
||||||
|
"@calcom/emails",
|
||||||
|
"@calcom/trpc",
|
||||||
|
"@calcom/embed-core",
|
||||||
|
"@calcom/embed-react",
|
||||||
|
"@calcom/features",
|
||||||
|
"@calcom/lib",
|
||||||
|
"@calcom/prisma",
|
||||||
|
"@calcom/ui",
|
||||||
|
],
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"@radix-ui/react-slider": "^1.0.0",
|
"@radix-ui/react-slider": "^1.0.0",
|
||||||
"@radix-ui/react-switch": "^1.0.0",
|
"@radix-ui/react-switch": "^1.0.0",
|
||||||
"@radix-ui/react-tooltip": "^1.0.0",
|
"@radix-ui/react-tooltip": "^1.0.0",
|
||||||
"next": "^13.1.1",
|
"next": "^13.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"storybook-addon-rtl-direction": "^0.0.19"
|
"storybook-addon-rtl-direction": "^0.0.19"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"highlight.js": "^11.6.0",
|
"highlight.js": "^11.6.0",
|
||||||
"isarray": "2.0.5",
|
"isarray": "2.0.5",
|
||||||
"next": "^13.1.1",
|
"next": "^13.2.1",
|
||||||
"openapi-snippet": "^0.13.0",
|
"openapi-snippet": "^0.13.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import NavTabs from "./NavTabs";
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
name: "profile",
|
name: "profile",
|
||||||
href: "/settings/profile",
|
href: "/settings/my-account/profile",
|
||||||
icon: FiUser,
|
icon: FiUser,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -303,6 +303,11 @@ const Component = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ShellHeading = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
return <span className="block py-2">{t("app_store")}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
export default function App(props: {
|
export default function App(props: {
|
||||||
name: string;
|
name: string;
|
||||||
description: AppType["description"];
|
description: AppType["description"];
|
||||||
@@ -328,10 +333,8 @@ export default function App(props: {
|
|||||||
images?: string[];
|
images?: string[];
|
||||||
isTemplate?: boolean;
|
isTemplate?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell smallHeading isPublic heading={t("app_store")} backPath="/apps" withoutSeo>
|
<Shell smallHeading isPublic heading={<ShellHeading />} backPath="/apps" withoutSeo>
|
||||||
<HeadSeo
|
<HeadSeo
|
||||||
title={props.name}
|
title={props.name}
|
||||||
description={props.description}
|
description={props.description}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { SkeletonText } from "@calcom/ui";
|
import { Button, SkeletonText } from "@calcom/ui";
|
||||||
|
import { FiMoreHorizontal } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
@@ -18,17 +19,21 @@ export default SkeletonLoader;
|
|||||||
|
|
||||||
function SkeletonItem() {
|
function SkeletonItem() {
|
||||||
return (
|
return (
|
||||||
<li className="group flex w-full items-center justify-between px-2 py-[23px] sm:px-6">
|
<li>
|
||||||
<div className="flex-grow truncate text-sm">
|
<div className="flex items-center justify-between py-5 hover:bg-neutral-50 ltr:pl-4 rtl:pr-4 sm:ltr:pl-0 sm:rtl:pr-0">
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="items-between flex w-full flex-col justify-center hover:bg-neutral-50 sm:px-6">
|
||||||
<SkeletonText className="h-4 w-32" />
|
<SkeletonText className="my-1 h-4 w-32" />
|
||||||
<SkeletonText className="h-2 w-32" />
|
<SkeletonText className="my-1 h-2 w-24" />
|
||||||
</div>
|
<SkeletonText className="my-1 h-2 w-40" />
|
||||||
</div>
|
|
||||||
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 lg:flex">
|
|
||||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
|
||||||
<SkeletonText className="h-6 w-12" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
className="mx-5"
|
||||||
|
type="button"
|
||||||
|
variant="icon"
|
||||||
|
color="secondary"
|
||||||
|
StartIcon={FiMoreHorizontal}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
|
|
||||||
import type { Dayjs } from "@calcom/dayjs";
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { nameOfDay } from "@calcom/lib/weekday";
|
import { nameOfDay } from "@calcom/lib/weekday";
|
||||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
||||||
@@ -50,6 +51,15 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBrand(getComputedStyle(document.documentElement).getPropertyValue("--brand-color").trim());
|
setBrand(getComputedStyle(document.documentElement).getPropertyValue("--brand-color").trim());
|
||||||
}, []);
|
}, []);
|
||||||
|
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||||
|
const ref = useCallback(
|
||||||
|
(node: HTMLDivElement) => {
|
||||||
|
if (isMobile) {
|
||||||
|
node?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isMobile]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={slotPickerRef}>
|
<div ref={slotPickerRef}>
|
||||||
@@ -75,7 +85,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-grow overflow-y-auto sm:block md:h-[364px]">
|
<div ref={ref} className="flex-grow overflow-y-auto sm:block md:h-[364px]">
|
||||||
{slots.length > 0 &&
|
{slots.length > 0 &&
|
||||||
slots.map((slot) => {
|
slots.map((slot) => {
|
||||||
type BookingURL = {
|
type BookingURL = {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import dayjs from "@calcom/dayjs";
|
|||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Badge } from "@calcom/ui";
|
import { Badge } from "@calcom/ui";
|
||||||
import { FiCheckSquare, FiClock, FiInfo } from "@calcom/ui/components/icon";
|
import { FiCheckSquare, FiClock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
@@ -94,15 +94,7 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
"flex",
|
"flex",
|
||||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
||||||
)}>
|
)}>
|
||||||
<div>
|
<div className="scroll-bar scrollbar-track-w-20 max-h-[200px] max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
|
||||||
<FiInfo
|
|
||||||
className={classNames(
|
|
||||||
"ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]",
|
|
||||||
isBookingPage && "dark:text-darkgray-600 -mt-1 text-gray-500"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="max-w-[calc(100%_-_2rem)] flex-shrink break-words">
|
|
||||||
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
|
|
||||||
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
|
import { FiCreditCard } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
const BookingDescriptionPayment = (props: { eventType: Parameters<typeof getPaymentAppData>[0] }) => {
|
||||||
|
const paymentAppData = getPaymentAppData(props.eventType);
|
||||||
|
if (!paymentAppData || paymentAppData.price <= 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||||
|
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<FormattedNumber
|
||||||
|
value={paymentAppData.price / 100.0}
|
||||||
|
style="currency"
|
||||||
|
currency={paymentAppData.currency?.toUpperCase()}
|
||||||
|
/>
|
||||||
|
</IntlProvider>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BookingDescriptionPayment;
|
||||||
@@ -223,6 +223,8 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
};
|
};
|
||||||
const showRecordingsButtons =
|
const showRecordingsButtons =
|
||||||
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
||||||
|
|
||||||
|
const title = decodeURIComponent(booking.title);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RescheduleDialog
|
<RescheduleDialog
|
||||||
@@ -355,12 +357,12 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
|
|
||||||
<div className="cursor-pointer py-4">
|
<div className="cursor-pointer py-4">
|
||||||
<div
|
<div
|
||||||
title={booking.title}
|
title={title}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"max-w-10/12 sm:max-w-56 text-sm font-medium leading-6 text-gray-900 md:max-w-full",
|
"max-w-10/12 sm:max-w-56 text-sm font-medium leading-6 text-gray-900 md:max-w-full",
|
||||||
isCancelled ? "line-through" : ""
|
isCancelled ? "line-through" : ""
|
||||||
)}>
|
)}>
|
||||||
{booking.title}
|
{title}
|
||||||
<span> </span>
|
<span> </span>
|
||||||
|
|
||||||
{!!booking?.eventType?.price && !booking.paid && (
|
{!!booking?.eventType?.price && !booking.paid && (
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function TimezoneDropdown({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white">
|
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit max-w-[20rem] items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white">
|
||||||
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
|
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
|
||||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -4,7 +4,7 @@ import { isValidPhoneNumber } from "libphonenumber-js";
|
|||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch, useFormContext } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { EventLocationType, LocationObject } from "@calcom/app-store/locations";
|
import type { EventLocationType, LocationObject } from "@calcom/app-store/locations";
|
||||||
@@ -49,16 +49,19 @@ const LocationInput = (props: {
|
|||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
}): JSX.Element | null => {
|
}): JSX.Element | null => {
|
||||||
const { eventLocationType, locationFormMethods, ...remainingProps } = props;
|
const { eventLocationType, locationFormMethods, ...remainingProps } = props;
|
||||||
|
const { control } = useFormContext() as typeof locationFormMethods;
|
||||||
if (eventLocationType?.organizerInputType === "text") {
|
if (eventLocationType?.organizerInputType === "text") {
|
||||||
return (
|
return (
|
||||||
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
|
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
|
||||||
);
|
);
|
||||||
} else if (eventLocationType?.organizerInputType === "phone") {
|
} else if (eventLocationType?.organizerInputType === "phone") {
|
||||||
return (
|
return (
|
||||||
<PhoneInput
|
<Controller
|
||||||
name={eventLocationType.variable}
|
name={eventLocationType.variable}
|
||||||
control={locationFormMethods.control}
|
control={control}
|
||||||
{...remainingProps}
|
render={({ field: { onChange, value } }) => {
|
||||||
|
return <PhoneInput onChange={onChange} value={value} {...remainingProps} />;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ const EventTypeScheduleDetails = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const filterDays = (dayNum: number) =>
|
const filterDays = (dayNum: number) =>
|
||||||
schedule?.schedule.availability.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
|
schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 rounded border px-6 pb-4">
|
<div className="space-y-4 rounded border px-6 pb-4">
|
||||||
@@ -156,15 +156,17 @@ const EventTypeScheduleDetails = () => {
|
|||||||
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
|
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
|
||||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
{!!schedule?.id && (
|
||||||
href={`/availability/${schedule?.schedule.id}`}
|
<Button
|
||||||
disabled={isLoading}
|
href={`/availability/${schedule.id}`}
|
||||||
color="minimal"
|
disabled={isLoading}
|
||||||
EndIcon={FiExternalLink}
|
color="minimal"
|
||||||
target="_blank"
|
EndIcon={FiExternalLink}
|
||||||
rel="noopener noreferrer">
|
target="_blank"
|
||||||
{t("edit_availability")}
|
rel="noopener noreferrer">
|
||||||
</Button>
|
{t("edit_availability")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import type { FC } from "react";
|
||||||
|
import type { SubmitHandler } from "react-hook-form";
|
||||||
|
import { FormProvider } from "react-hook-form";
|
||||||
|
import { useForm, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import type { EventNameObjectType } from "@calcom/core/event";
|
||||||
|
import { getEventName } from "@calcom/core/event";
|
||||||
|
import { validateCustomEventName } from "@calcom/core/event";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Button, Dialog, DialogClose, DialogFooter, DialogContent, TextField } from "@calcom/ui";
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
customEventName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomEventTypeModalFormProps {
|
||||||
|
placeHolder: string;
|
||||||
|
close: () => void;
|
||||||
|
setValue: (value: string) => void;
|
||||||
|
event: EventNameObjectType;
|
||||||
|
defaultValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomEventTypeModalForm: FC<CustomEventTypeModalFormProps> = (props) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { placeHolder, close, setValue, event } = props;
|
||||||
|
const { register, handleSubmit, watch, getValues } = useFormContext<FormValues>();
|
||||||
|
const onSubmit: SubmitHandler<FormValues> = (data) => {
|
||||||
|
setValue(data.customEventName);
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
// const customEventName = watch("customEventName");
|
||||||
|
const previewText = getEventName({ ...event, eventName: watch("customEventName") });
|
||||||
|
const placeHolder_ = watch("customEventName") === "" ? previewText : placeHolder;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
id="custom-event-name"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const isEmpty = getValues("customEventName") === "";
|
||||||
|
if (isEmpty) {
|
||||||
|
setValue("");
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(onSubmit)(e);
|
||||||
|
}}>
|
||||||
|
<TextField
|
||||||
|
label={t("event_name_in_calendar")}
|
||||||
|
type="text"
|
||||||
|
placeholder={placeHolder_}
|
||||||
|
{...register("customEventName", {
|
||||||
|
validate: (value) => validateCustomEventName(value, t("invalid_event_name_variables")),
|
||||||
|
})}
|
||||||
|
className="mb-0"
|
||||||
|
/>
|
||||||
|
<div className="pt-6 text-sm">
|
||||||
|
<div className="mb-6 rounded-md bg-gray-100 p-2">
|
||||||
|
<h1 className="mb-2 ml-1 font-medium text-gray-900">{t("available_variables")}</h1>
|
||||||
|
<div className="mb-2.5 flex font-normal">
|
||||||
|
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Event type title}`}</p>
|
||||||
|
<p className="text-gray-900">{t("event_name_info")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2.5 flex font-normal">
|
||||||
|
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Organiser}`}</p>
|
||||||
|
<p className="text-gray-900">{t("your_full_name")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2.5 flex font-normal">
|
||||||
|
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Scheduler}`}</p>
|
||||||
|
<p className="text-gray-900">{t("scheduler_full_name")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-1 flex font-normal">
|
||||||
|
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Location}`}</p>
|
||||||
|
<p className="text-gray-900">{t("location_info")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 className="mb-2 text-[14px] font-medium leading-4">{t("preview")}</h1>
|
||||||
|
<div
|
||||||
|
className="flex h-[212px] w-full rounded-md border-y bg-cover bg-center"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url(/calendar-preview.svg)",
|
||||||
|
}}>
|
||||||
|
<div className="m-auto flex items-center justify-center self-stretch">
|
||||||
|
<div className="mt-3 ml-11 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black bg-gray-100 text-[12px] leading-3">
|
||||||
|
<p className="overflow-hidden text-ellipsis p-1.5 font-medium text-gray-900">{previewText}</p>
|
||||||
|
<p className="ml-1.5 text-[10px] font-normal text-gray-600">8 - 10 AM</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CustomEventTypeModalProps {
|
||||||
|
placeHolder: string;
|
||||||
|
defaultValue: string;
|
||||||
|
close: () => void;
|
||||||
|
setValue: (value: string) => void;
|
||||||
|
event: EventNameObjectType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomEventTypeModal: FC<CustomEventTypeModalProps> = (props) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const { defaultValue, placeHolder, close, setValue, event } = props;
|
||||||
|
|
||||||
|
const methods = useForm<FormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
customEventName: defaultValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={true} onOpenChange={close}>
|
||||||
|
<DialogContent
|
||||||
|
title={t("custom_event_name")}
|
||||||
|
description={t("custom_event_name_description")}
|
||||||
|
type="creation"
|
||||||
|
enableOverflow>
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<CustomEventTypeModalForm
|
||||||
|
event={event}
|
||||||
|
close={close}
|
||||||
|
setValue={setValue}
|
||||||
|
placeHolder={placeHolder}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
/>
|
||||||
|
</FormProvider>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose>{t("cancel")}</DialogClose>
|
||||||
|
|
||||||
|
<Button form="custom-event-name" type="submit" color="primary">
|
||||||
|
{t("create")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomEventTypeModal;
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
|
||||||
import { EventTypeCustomInputType } from "@prisma/client";
|
|
||||||
import type { CustomInputParsed } from "pages/event-types/[type]";
|
|
||||||
import type { FC } from "react";
|
|
||||||
import type { Control, UseFormRegister } from "react-hook-form";
|
|
||||||
import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { Button, Label, Select, TextField } from "@calcom/ui";
|
|
||||||
import { FiPlus, FiX } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
interface OptionTypeBase {
|
|
||||||
label: string;
|
|
||||||
value: EventTypeCustomInputType;
|
|
||||||
options?: { label: string; type: string }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onSubmit: (output: CustomInputParsed) => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
selectedCustomInput?: CustomInputParsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
type IFormInput = CustomInputParsed;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Getting a random ID gives us the option to know WHICH field is changed
|
|
||||||
* when the user edits a custom field.
|
|
||||||
* This UUID is only used to check for changes in the UI and not the ID we use in the DB
|
|
||||||
* There is very very very slim chance that this will cause a collision
|
|
||||||
* */
|
|
||||||
const randomId = () => Math.floor(Math.random() * 1000000 + new Date().getTime());
|
|
||||||
|
|
||||||
const CustomInputTypeForm: FC<Props> = (props) => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const inputOptions: OptionTypeBase[] = [
|
|
||||||
{ value: EventTypeCustomInputType.TEXT, label: t("text") },
|
|
||||||
{ value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") },
|
|
||||||
{ value: EventTypeCustomInputType.NUMBER, label: t("number") },
|
|
||||||
{ value: EventTypeCustomInputType.BOOL, label: t("checkbox") },
|
|
||||||
{
|
|
||||||
value: EventTypeCustomInputType.RADIO,
|
|
||||||
label: t("radio"),
|
|
||||||
},
|
|
||||||
{ value: EventTypeCustomInputType.PHONE, label: t("phone_number") },
|
|
||||||
];
|
|
||||||
|
|
||||||
const { selectedCustomInput } = props;
|
|
||||||
|
|
||||||
const defaultValues = selectedCustomInput
|
|
||||||
? { ...selectedCustomInput, id: selectedCustomInput?.id || randomId() }
|
|
||||||
: {
|
|
||||||
id: randomId(),
|
|
||||||
type: EventTypeCustomInputType.TEXT,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { register, control, getValues } = useForm<IFormInput>({
|
|
||||||
defaultValues,
|
|
||||||
});
|
|
||||||
const selectedInputType = useWatch({ name: "type", control });
|
|
||||||
const selectedInputOption = inputOptions.find((e) => selectedInputType === e.value);
|
|
||||||
|
|
||||||
const onCancel = () => {
|
|
||||||
props.onCancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("input_type")}
|
|
||||||
</label>
|
|
||||||
<Controller
|
|
||||||
name="type"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Select
|
|
||||||
id="type"
|
|
||||||
defaultValue={selectedInputOption}
|
|
||||||
options={inputOptions}
|
|
||||||
isSearchable={false}
|
|
||||||
className="mt-1 mb-2 block w-full min-w-0 flex-1 text-sm"
|
|
||||||
onChange={(option) => option && field.onChange(option.value)}
|
|
||||||
value={selectedInputOption}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<TextField
|
|
||||||
label={t("label")}
|
|
||||||
type="text"
|
|
||||||
id="label"
|
|
||||||
required
|
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
defaultValue={selectedCustomInput?.label}
|
|
||||||
{...register("label", { required: true })}
|
|
||||||
/>
|
|
||||||
{(selectedInputType === EventTypeCustomInputType.TEXT ||
|
|
||||||
selectedInputType === EventTypeCustomInputType.TEXTLONG) && (
|
|
||||||
<TextField
|
|
||||||
label={t("placeholder")}
|
|
||||||
type="text"
|
|
||||||
id="placeholder"
|
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
defaultValue={selectedCustomInput?.placeholder}
|
|
||||||
{...register("placeholder")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{selectedInputType === EventTypeCustomInputType.RADIO && (
|
|
||||||
<RadioInputHandler control={control} register={register} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex h-5 items-center">
|
|
||||||
<input
|
|
||||||
id="required"
|
|
||||||
type="checkbox"
|
|
||||||
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300 ltr:mr-2 rtl:ml-2"
|
|
||||||
defaultChecked={selectedCustomInput?.required ?? true}
|
|
||||||
{...register("required")}
|
|
||||||
/>
|
|
||||||
<label htmlFor="required" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("is_required")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
id="eventTypeId"
|
|
||||||
value={selectedCustomInput?.eventTypeId || -1}
|
|
||||||
{...register("eventTypeId", { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
id="id"
|
|
||||||
value={selectedCustomInput?.id || -1}
|
|
||||||
{...register("id", { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<div className="mt-5 flex justify-end space-x-2 rtl:space-x-reverse sm:mt-4">
|
|
||||||
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2 rtl:ml-2">
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
props.onSubmit(getValues());
|
|
||||||
}}>
|
|
||||||
{t("save")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function RadioInputHandler({
|
|
||||||
register,
|
|
||||||
control,
|
|
||||||
}: {
|
|
||||||
register: UseFormRegister<IFormInput>;
|
|
||||||
control: Control<IFormInput>;
|
|
||||||
}) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const { fields, append, remove } = useFieldArray<IFormInput>({
|
|
||||||
control,
|
|
||||||
name: "options",
|
|
||||||
shouldUnregister: true,
|
|
||||||
});
|
|
||||||
const [animateRef] = useAutoAnimate<HTMLUListElement>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col ">
|
|
||||||
<Label htmlFor="radio_options">{t("options")}</Label>
|
|
||||||
<ul
|
|
||||||
className="flex max-h-80 w-full flex-col space-y-1 overflow-y-scroll rounded-md bg-gray-50 p-4"
|
|
||||||
ref={animateRef}>
|
|
||||||
<>
|
|
||||||
{fields.map((option, index) => (
|
|
||||||
<li key={`${option.id}`}>
|
|
||||||
<TextField
|
|
||||||
id={option.id}
|
|
||||||
placeholder={t("enter_option", { index: index + 1 })}
|
|
||||||
addOnFilled={false}
|
|
||||||
label={t("option", { index: index + 1 })}
|
|
||||||
labelSrOnly
|
|
||||||
{...register(`options.${index}.label` as const, { required: true })}
|
|
||||||
addOnSuffix={
|
|
||||||
<Button
|
|
||||||
variant="icon"
|
|
||||||
color="minimal"
|
|
||||||
StartIcon={FiX}
|
|
||||||
onClick={() => {
|
|
||||||
remove(index);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
color="minimal"
|
|
||||||
StartIcon={FiPlus}
|
|
||||||
className="!text-sm !font-medium"
|
|
||||||
onClick={() => {
|
|
||||||
append({ label: "", type: "text" });
|
|
||||||
}}>
|
|
||||||
{t("add_an_option")}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CustomInputTypeForm;
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import dynamic from "next/dynamic";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { CustomInputParsed, EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import short from "short-uuid";
|
import short from "short-uuid";
|
||||||
@@ -8,30 +9,17 @@ import { v5 as uuidv5 } from "uuid";
|
|||||||
import type { EventNameObjectType } from "@calcom/core/event";
|
import type { EventNameObjectType } from "@calcom/core/event";
|
||||||
import { getEventName } from "@calcom/core/event";
|
import { getEventName } from "@calcom/core/event";
|
||||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import CustomInputItem from "@calcom/features/eventtypes/components/CustomInputItem";
|
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||||
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import {
|
import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
|
||||||
Badge,
|
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
Label,
|
|
||||||
SettingsToggle,
|
|
||||||
showToast,
|
|
||||||
TextField,
|
|
||||||
Tooltip,
|
|
||||||
} from "@calcom/ui";
|
|
||||||
import { FiEdit, FiCopy, FiPlus } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
|
|
||||||
|
|
||||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||||
|
|
||||||
|
const CustomEventTypeModal = dynamic(() => import("@components/eventtype/CustomEventTypeModal"));
|
||||||
|
|
||||||
const generateHashedLink = (id: number) => {
|
const generateHashedLink = (id: number) => {
|
||||||
const translator = short();
|
const translator = short();
|
||||||
const seed = `${id}:${new Date().getTime()}`;
|
const seed = `${id}:${new Date().getTime()}`;
|
||||||
@@ -39,22 +27,11 @@ const generateHashedLink = (id: number) => {
|
|||||||
return uid;
|
return uid;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRandomId = (length = 8) => {
|
|
||||||
return (
|
|
||||||
-1 *
|
|
||||||
parseInt(
|
|
||||||
Math.ceil(Math.random() * Date.now())
|
|
||||||
.toPrecision(length)
|
|
||||||
.toString()
|
|
||||||
.replace(".", "")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps, "eventType" | "team">) => {
|
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps, "eventType" | "team">) => {
|
||||||
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
|
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const [showEventNameTip, setShowEventNameTip] = useState(false);
|
const [showEventNameTip, setShowEventNameTip] = useState(false);
|
||||||
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
||||||
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
||||||
@@ -66,41 +43,39 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
host: eventType.users[0]?.name || "Nameless",
|
host: eventType.users[0]?.name || "Nameless",
|
||||||
t,
|
t,
|
||||||
};
|
};
|
||||||
const [previewText, setPreviewText] = useState(getEventName(eventNameObject));
|
|
||||||
const [customInputs, setCustomInputs] = useState<CustomInputParsed[]>(
|
|
||||||
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
|
||||||
);
|
|
||||||
const [selectedCustomInput, setSelectedCustomInput] = useState<CustomInputParsed | undefined>(undefined);
|
|
||||||
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
|
||||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||||
|
|
||||||
const removeCustom = (index: number) => {
|
|
||||||
formMethods.getValues("customInputs").splice(index, 1);
|
|
||||||
customInputs.splice(index, 1);
|
|
||||||
setCustomInputs([...customInputs]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changePreviewText = (eventNameObject: EventNameObjectType, previewEventName: string) => {
|
|
||||||
setPreviewText(
|
|
||||||
previewEventName
|
|
||||||
.replace("{Event type title}", eventNameObject.eventType)
|
|
||||||
.replace("{Scheduler}", eventNameObject.attendeeName)
|
|
||||||
.replace("{Organiser}", eventNameObject.host)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
||||||
}, [eventType.users, hashedUrl, team?.id]);
|
}, [eventType.users, hashedUrl, team?.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
const toggleGuests = (enabled: boolean) => {
|
||||||
if (eventType.customInputs) {
|
const bookingFields = formMethods.getValues("bookingFields");
|
||||||
setCustomInputs(eventType.customInputs.sort((a, b) => a.id - b.id));
|
formMethods.setValue(
|
||||||
}
|
"bookingFields",
|
||||||
}, [eventType.customInputs]);
|
bookingFields.map((field) => {
|
||||||
|
if (field.name === "guests") {
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
hidden: !enabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return field;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventNamePlaceholder = getEventName({
|
||||||
|
...eventNameObject,
|
||||||
|
eventName: formMethods.watch("eventName"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeEventNameTip = () => setShowEventNameTip(false);
|
||||||
|
|
||||||
|
const setEventName = (value: string) => formMethods.setValue("eventName", value);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-8">
|
<div className="flex flex-col space-y-8">
|
||||||
{/**
|
{/**
|
||||||
@@ -141,16 +116,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<TextField
|
<TextField
|
||||||
label={t("event_name_in_calendar")}
|
label={t("event_name_in_calendar")}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t("meeting_with_user")}
|
placeholder={eventNamePlaceholder}
|
||||||
defaultValue={eventType.eventName || ""}
|
defaultValue={eventType.eventName || ""}
|
||||||
{...formMethods.register("eventName", {
|
{...formMethods.register("eventName")}
|
||||||
onChange: (e) => {
|
|
||||||
if (!e.target.value || !formMethods.getValues("eventName")) {
|
|
||||||
return setPreviewText(getEventName(eventNameObject));
|
|
||||||
}
|
|
||||||
changePreviewText(eventNameObject, e.target.value);
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
addOnSuffix={
|
addOnSuffix={
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -159,53 +127,18 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
color="minimal"
|
color="minimal"
|
||||||
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
|
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
|
||||||
onClick={() => setShowEventNameTip((old) => !old)}
|
onClick={() => setShowEventNameTip((old) => !old)}
|
||||||
|
aria-label="edit custom name"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div className="">
|
<FormBuilder
|
||||||
<SettingsToggle
|
title={t("booking_questions_title")}
|
||||||
title={t("additional_inputs")}
|
description={t("booking_questions_description")}
|
||||||
description={t("additional_input_description")}
|
addFieldLabel={t("add_a_booking_question")}
|
||||||
checked={customInputs.length > 0}
|
formProp="bookingFields"
|
||||||
onCheckedChange={(e) => {
|
/>
|
||||||
if (e && customInputs.length === 0) {
|
|
||||||
// Push a placeholders
|
|
||||||
setSelectedCustomInputModalOpen(true);
|
|
||||||
} else if (!e) {
|
|
||||||
formMethods.setValue("customInputs", []);
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<ul className="my-4 rounded-md border">
|
|
||||||
{customInputs.map((customInput, idx) => (
|
|
||||||
<CustomInputItem
|
|
||||||
key={idx}
|
|
||||||
question={customInput.label}
|
|
||||||
type={customInput.type}
|
|
||||||
required={customInput.required}
|
|
||||||
editOnClick={() => {
|
|
||||||
setSelectedCustomInput(customInput);
|
|
||||||
setSelectedCustomInputModalOpen(true);
|
|
||||||
}}
|
|
||||||
deleteOnClick={() => removeCustom(idx)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
{customInputs.length > 0 && (
|
|
||||||
<Button
|
|
||||||
StartIcon={FiPlus}
|
|
||||||
color="minimal"
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedCustomInput(undefined);
|
|
||||||
setSelectedCustomInputModalOpen(true);
|
|
||||||
}}>
|
|
||||||
{t("add_input")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</SettingsToggle>
|
|
||||||
</div>
|
|
||||||
<hr />
|
<hr />
|
||||||
<RequiresConfirmationController
|
<RequiresConfirmationController
|
||||||
seatsEnabled={seatsEnabled}
|
seatsEnabled={seatsEnabled}
|
||||||
@@ -213,22 +146,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
requiresConfirmation={requiresConfirmation}
|
requiresConfirmation={requiresConfirmation}
|
||||||
onRequiresConfirmation={setRequiresConfirmation}
|
onRequiresConfirmation={setRequiresConfirmation}
|
||||||
/>
|
/>
|
||||||
<hr />
|
|
||||||
<Controller
|
|
||||||
name="disableGuests"
|
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={eventType.disableGuests}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<SettingsToggle
|
|
||||||
title={t("disable_guests")}
|
|
||||||
description={t("disable_guests_description")}
|
|
||||||
checked={value}
|
|
||||||
onCheckedChange={(e) => onChange(e)}
|
|
||||||
disabled={seatsEnabled}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
<Controller
|
<Controller
|
||||||
name="hideCalendarNotes"
|
name="hideCalendarNotes"
|
||||||
@@ -244,22 +161,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
<Controller
|
|
||||||
name="metadata.additionalNotesRequired"
|
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={!!eventType.metadata.additionalNotesRequired}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<div className="flex space-x-3 ">
|
|
||||||
<SettingsToggle
|
|
||||||
title={t("require_additional_notes")}
|
|
||||||
description={t("require_additional_notes_description")}
|
|
||||||
checked={!!value}
|
|
||||||
onCheckedChange={(e) => onChange(e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<hr />
|
|
||||||
<Controller
|
<Controller
|
||||||
name="successRedirectUrl"
|
name="successRedirectUrl"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -360,13 +261,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
// Enabling seats will disable guests and requiring confirmation until fully supported
|
// Enabling seats will disable guests and requiring confirmation until fully supported
|
||||||
if (e) {
|
if (e) {
|
||||||
formMethods.setValue("disableGuests", true);
|
toggleGuests(false);
|
||||||
formMethods.setValue("requiresConfirmation", false);
|
formMethods.setValue("requiresConfirmation", false);
|
||||||
setRequiresConfirmation(false);
|
setRequiresConfirmation(false);
|
||||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
formMethods.setValue("seatsPerTimeSlot", 2);
|
||||||
} else {
|
} else {
|
||||||
formMethods.setValue("seatsPerTimeSlot", null);
|
formMethods.setValue("seatsPerTimeSlot", null);
|
||||||
formMethods.setValue("disableGuests", false);
|
toggleGuests(true);
|
||||||
}
|
}
|
||||||
onChange(e);
|
onChange(e);
|
||||||
}}>
|
}}>
|
||||||
@@ -404,130 +305,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{showEventNameTip && (
|
{showEventNameTip && (
|
||||||
<Dialog open={showEventNameTip} onOpenChange={setShowEventNameTip}>
|
<CustomEventTypeModal
|
||||||
<DialogContent
|
close={closeEventNameTip}
|
||||||
title={t("custom_event_name")}
|
setValue={setEventName}
|
||||||
description={t("custom_event_name_description")}
|
defaultValue={eventType.eventName || ""}
|
||||||
type="creation"
|
placeHolder={eventNamePlaceholder}
|
||||||
enableOverflow>
|
event={eventNameObject}
|
||||||
<TextField
|
/>
|
||||||
label={t("event_name_in_calendar")}
|
|
||||||
type="text"
|
|
||||||
placeholder={t("meeting_with_user")}
|
|
||||||
defaultValue={eventType.eventName || ""}
|
|
||||||
{...formMethods.register("eventName", {
|
|
||||||
onChange: (e) => {
|
|
||||||
if (!e.target.value || !formMethods.getValues("eventName")) {
|
|
||||||
return setPreviewText(getEventName(eventNameObject));
|
|
||||||
}
|
|
||||||
changePreviewText(eventNameObject, e.target.value);
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
className="mb-0"
|
|
||||||
/>
|
|
||||||
<div className="text-sm">
|
|
||||||
<div className="mb-6 rounded-md bg-gray-100 p-2">
|
|
||||||
<h1 className="mb-2 ml-1 font-medium text-gray-900">{t("available_variables")}</h1>
|
|
||||||
<div className="mb-2.5 flex font-normal">
|
|
||||||
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Event type title}`}</p>
|
|
||||||
<p className="text-gray-900">{t("event_name_info")}</p>
|
|
||||||
</div>
|
|
||||||
<div className="mb-2.5 flex font-normal">
|
|
||||||
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Organiser}`}</p>
|
|
||||||
<p className="text-gray-900">{t("your_full_name")}</p>
|
|
||||||
</div>
|
|
||||||
<div className="mb-2.5 flex font-normal">
|
|
||||||
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Scheduler}`}</p>
|
|
||||||
<p className="text-gray-900">{t("scheduler_full_name")}</p>
|
|
||||||
</div>
|
|
||||||
<div className="mb-1 flex font-normal">
|
|
||||||
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Location}`}</p>
|
|
||||||
<p className="text-gray-900">{t("location_info")}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h1 className="mb-2 text-[14px] font-medium leading-4">{t("preview")}</h1>
|
|
||||||
<div
|
|
||||||
className="flex h-[212px] w-full rounded-md border-y bg-cover bg-center"
|
|
||||||
style={{
|
|
||||||
backgroundImage: "url(/calendar-preview.svg)",
|
|
||||||
}}>
|
|
||||||
<div className="m-auto flex items-center justify-center self-stretch">
|
|
||||||
<div className="mt-3 ml-11 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black bg-gray-100 text-[12px] leading-3">
|
|
||||||
<p className="overflow-hidden text-ellipsis p-1.5 font-medium text-gray-900">
|
|
||||||
{previewText}
|
|
||||||
</p>
|
|
||||||
<p className="ml-1.5 text-[10px] font-normal text-gray-600">8 - 10 AM</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose onClick={() => formMethods.setValue("eventName", eventType.eventName ?? "")}>
|
|
||||||
{t("cancel")}
|
|
||||||
</DialogClose>
|
|
||||||
<Button color="primary" onClick={() => setShowEventNameTip(false)}>
|
|
||||||
{t("create")}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
)}
|
||||||
<Controller
|
|
||||||
name="customInputs"
|
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={customInputs}
|
|
||||||
render={() => (
|
|
||||||
<Dialog open={selectedCustomInputModalOpen} onOpenChange={setSelectedCustomInputModalOpen}>
|
|
||||||
<DialogContent
|
|
||||||
type="creation"
|
|
||||||
Icon={FiPlus}
|
|
||||||
title={t("add_new_custom_input_field")}
|
|
||||||
description={t("this_input_will_shown_booking_this_event")}>
|
|
||||||
<CustomInputTypeForm
|
|
||||||
selectedCustomInput={selectedCustomInput}
|
|
||||||
onSubmit={(values) => {
|
|
||||||
const customInput: CustomInputParsed = {
|
|
||||||
id: getRandomId(),
|
|
||||||
eventTypeId: -1,
|
|
||||||
label: values.label,
|
|
||||||
placeholder: values.placeholder,
|
|
||||||
required: values.required,
|
|
||||||
type: values.type,
|
|
||||||
options: values.options,
|
|
||||||
hasToBeCreated: true,
|
|
||||||
};
|
|
||||||
if (selectedCustomInput) {
|
|
||||||
selectedCustomInput.label = customInput.label;
|
|
||||||
selectedCustomInput.placeholder = customInput.placeholder;
|
|
||||||
selectedCustomInput.required = customInput.required;
|
|
||||||
selectedCustomInput.type = customInput.type;
|
|
||||||
selectedCustomInput.options = customInput.options || undefined;
|
|
||||||
selectedCustomInput.hasToBeCreated = false;
|
|
||||||
// Update by id
|
|
||||||
const inputIndex = customInputs.findIndex((input) => input.id === values.id);
|
|
||||||
customInputs[inputIndex] = selectedCustomInput;
|
|
||||||
setCustomInputs(customInputs);
|
|
||||||
formMethods.setValue("customInputs", customInputs);
|
|
||||||
} else {
|
|
||||||
const concatted = customInputs.concat({
|
|
||||||
...customInput,
|
|
||||||
options: customInput.options,
|
|
||||||
});
|
|
||||||
console.log(concatted);
|
|
||||||
setCustomInputs(concatted);
|
|
||||||
formMethods.setValue("customInputs", concatted);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedCustomInputModalOpen(false);
|
|
||||||
}}
|
|
||||||
onCancel={() => {
|
|
||||||
setSelectedCustomInputModalOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -13,8 +13,10 @@ import type { EventLocationType } from "@calcom/app-store/locations";
|
|||||||
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
import { slugify } from "@calcom/lib/slugify";
|
import { slugify } from "@calcom/lib/slugify";
|
||||||
import { Button, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
|
import turndown from "@calcom/lib/turndownService";
|
||||||
|
import { Button, Editor, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
|
||||||
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
|
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
@@ -44,11 +46,14 @@ const getDefaultLocationValue = (options: EventTypeSetupProps["locationOptions"]
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const EventSetupTab = (
|
export const EventSetupTab = (
|
||||||
props: Pick<EventTypeSetupProps, "eventType" | "locationOptions" | "team" | "teamMembers">
|
props: Pick<
|
||||||
|
EventTypeSetupProps,
|
||||||
|
"eventType" | "locationOptions" | "team" | "teamMembers" | "destinationCalendar"
|
||||||
|
>
|
||||||
) => {
|
) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const { eventType, locationOptions, team } = props;
|
const { eventType, locationOptions, team, destinationCalendar } = props;
|
||||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||||
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
||||||
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
||||||
@@ -229,7 +234,10 @@ export const EventSetupTab = (
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{validLocations.some((location) => location.type === MeetLocationType) && (
|
{validLocations.some(
|
||||||
|
(location) =>
|
||||||
|
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
|
||||||
|
) && (
|
||||||
<div className="flex text-sm text-gray-600">
|
<div className="flex text-sm text-gray-600">
|
||||||
<FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" />
|
<FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" />
|
||||||
<Trans i18nKey="event_type_requres_google_cal">
|
<Trans i18nKey="event_type_requres_google_cal">
|
||||||
@@ -241,12 +249,11 @@ export const EventSetupTab = (
|
|||||||
className="underline">
|
className="underline">
|
||||||
here.
|
here.
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
We will fall back to Cal video if you do not change it.
|
|
||||||
</p>
|
</p>
|
||||||
</Trans>
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{validLocations.length > 0 && validLocations.length !== locationOptions.length && (
|
{validLocations.length > 0 && (
|
||||||
<li>
|
<li>
|
||||||
<Button
|
<Button
|
||||||
data-testid="add-location"
|
data-testid="add-location"
|
||||||
@@ -272,12 +279,15 @@ export const EventSetupTab = (
|
|||||||
defaultValue={eventType.title}
|
defaultValue={eventType.title}
|
||||||
{...formMethods.register("title")}
|
{...formMethods.register("title")}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<div>
|
||||||
label={t("description")}
|
<Label>{t("description")}</Label>
|
||||||
placeholder={t("quick_video_meeting")}
|
<Editor
|
||||||
defaultValue={eventType.description ?? ""}
|
getText={() => md.render(formMethods.getValues("description") || eventType.description || "")}
|
||||||
{...formMethods.register("description")}
|
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
||||||
/>
|
excludedToolbarItems={["blockType"]}
|
||||||
|
placeholder={t("quick_video_meeting")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t("URL")}
|
label={t("URL")}
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ function EventTypeSingleLayout({
|
|||||||
heading={eventType.title}
|
heading={eventType.title}
|
||||||
CTA={
|
CTA={
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<div className="hidden items-center rounded-md px-2 sm:flex sm:hover:bg-gray-100">
|
<div className="hidden items-center rounded-md px-2 sm:hover:bg-gray-100 lg:flex">
|
||||||
<Skeleton
|
<Skeleton
|
||||||
as={Label}
|
as={Label}
|
||||||
htmlFor="hiddenSwitch"
|
htmlFor="hiddenSwitch"
|
||||||
@@ -206,7 +206,7 @@ function EventTypeSingleLayout({
|
|||||||
</Skeleton>
|
</Skeleton>
|
||||||
<Switch
|
<Switch
|
||||||
id="hiddenSwitch"
|
id="hiddenSwitch"
|
||||||
defaultChecked={formMethods.getValues("hidden")}
|
checked={formMethods.watch("hidden")}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
formMethods.setValue("hidden", e);
|
formMethods.setValue("hidden", e);
|
||||||
}}
|
}}
|
||||||
@@ -261,7 +261,7 @@ function EventTypeSingleLayout({
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button className="lg:hidden" StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
|
<Button className="lg:hidden" StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent style={{ minWidth: "200px" }}>
|
||||||
<DropdownMenuItem className="focus:ring-gray-100">
|
<DropdownMenuItem className="focus:ring-gray-100">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -293,17 +293,17 @@ function EventTypeSingleLayout({
|
|||||||
{t("delete")}
|
{t("delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator className="block sm:hidden" />
|
<DropdownMenuSeparator />
|
||||||
<div className="flex items-center rounded-md py-1.5 px-4 sm:hidden sm:hover:bg-gray-100">
|
<div className="flex h-9 flex-row items-center justify-between py-2 px-4 sm:hover:bg-gray-100">
|
||||||
<Skeleton
|
<Skeleton
|
||||||
as={Label}
|
as={Label}
|
||||||
htmlFor="hiddenSwitch"
|
htmlFor="hiddenSwitch"
|
||||||
className="mt-2 inline cursor-pointer self-center pr-2 sm:hidden">
|
className="mt-2 inline cursor-pointer self-center pr-2 ">
|
||||||
{t("hide_from_profile")}
|
{t("hide_from_profile")}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
<Switch
|
<Switch
|
||||||
id="hiddenSwitch"
|
id="hiddenSwitch"
|
||||||
defaultChecked={formMethods.getValues("hidden")}
|
checked={formMethods.watch("hidden")}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
formMethods.setValue("hidden", e);
|
formMethods.setValue("hidden", e);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
import { ArrowRightIcon } from "@heroicons/react/solid";
|
||||||
import MarkdownIt from "markdown-it";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { FormEvent } from "react";
|
import type { FormEvent } from "react";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import turndown from "@calcom/lib/turndownService";
|
import turndown from "@calcom/lib/turndownService";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -14,8 +14,6 @@ import { Avatar } from "@calcom/ui";
|
|||||||
|
|
||||||
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
||||||
|
|
||||||
const md = new MarkdownIt("default", { html: true, breaks: true });
|
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
bio: string;
|
bio: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ShieldCheckIcon } from "@calcom/ui/components/icon";
|
import { FiShield } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
|
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 sm:flex sm:items-start">
|
<div className="mb-4 sm:flex sm:items-start">
|
||||||
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
|
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
<ShieldCheckIcon className="h-6 w-6 text-white" />
|
<FiShield className="h-6 w-6 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
<h3 className="font-cal text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
<h3 className="font-cal text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import MarkdownIt from "markdown-it";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { TeamPageProps } from "pages/team/[slug]";
|
import type { TeamPageProps } from "pages/team/[slug]";
|
||||||
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
import { Avatar } from "@calcom/ui";
|
import { Avatar } from "@calcom/ui";
|
||||||
|
|
||||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
|
||||||
|
|
||||||
type TeamType = TeamPageProps["team"];
|
type TeamType = TeamPageProps["team"];
|
||||||
type MembersType = TeamType["members"];
|
type MembersType = TeamType["members"];
|
||||||
type MemberType = MembersType[number];
|
type MemberType = MembersType[number];
|
||||||
@@ -19,7 +17,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={member.id} href={`/${member.username}`}>
|
<Link key={member.id} href={`/${member.username}`}>
|
||||||
<div className="sm:min-w-80 sm:max-w-80 dark:bg-darkgray-200 dark:hover:bg-darkgray-300 group flex min-h-full flex-col space-y-2 rounded-md bg-white p-4 hover:cursor-pointer hover:bg-gray-50 ">
|
<div className="sm:min-w-80 sm:max-w-80 dark:bg-darkgray-200 dark:hover:bg-darkgray-300 group flex min-h-full flex-col space-y-2 rounded-md bg-white p-4 hover:cursor-pointer hover:bg-gray-50 ">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="md"
|
size="md"
|
||||||
alt={member.name || ""}
|
alt={member.name || ""}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { StarIcon as StarIconSolid } from "@heroicons/react/solid";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { debounce, noop } from "lodash";
|
import { debounce, noop } from "lodash";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -14,7 +15,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Input, Label } from "@calcom/ui";
|
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Input, Label } from "@calcom/ui";
|
||||||
import { FiCheck, FiEdit2, FiExternalLink, StarIconSolid } from "@calcom/ui/components/icon";
|
import { FiCheck, FiEdit2, FiExternalLink } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export enum UsernameChangeStatusEnum {
|
export enum UsernameChangeStatusEnum {
|
||||||
UPGRADE = "UPGRADE",
|
UPGRADE = "UPGRADE",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
User,
|
User,
|
||||||
VerificationToken,
|
VerificationToken,
|
||||||
} from "@prisma/client";
|
} from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||||
|
|
||||||
import { identityProviderNameMap } from "@lib/auth";
|
import { identityProviderNameMap } from "@lib/auth";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,56 @@
|
|||||||
import type { Prisma, PrismaClient } from "@prisma/client";
|
import type { Prisma, PrismaClient } from "@prisma/client";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
async function getBooking(prisma: PrismaClient, uid: string) {
|
import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||||
const booking = await prisma.booking.findFirst({
|
import slugify from "@calcom/lib/slugify";
|
||||||
|
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
|
type BookingSelect = {
|
||||||
|
description: true;
|
||||||
|
customInputs: true;
|
||||||
|
attendees: {
|
||||||
|
select: {
|
||||||
|
email: true;
|
||||||
|
name: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
location: true;
|
||||||
|
smsReminderNumber: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Backward Compatibility for booking created before we had managed booking questions
|
||||||
|
function getResponsesFromOldBooking(
|
||||||
|
rawBooking: Prisma.BookingGetPayload<{
|
||||||
|
select: BookingSelect;
|
||||||
|
}>
|
||||||
|
) {
|
||||||
|
const customInputs = rawBooking.customInputs || {};
|
||||||
|
const responses = Object.keys(customInputs).reduce((acc, label) => {
|
||||||
|
acc[slugify(label) as keyof typeof acc] = customInputs[label as keyof typeof customInputs];
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
return {
|
||||||
|
// It is possible to have no attendees in a booking when the booking is cancelled.
|
||||||
|
name: rawBooking.attendees[0]?.name || "Nameless",
|
||||||
|
email: rawBooking.attendees[0]?.email || "",
|
||||||
|
guests: rawBooking.attendees.slice(1).map((attendee) => {
|
||||||
|
return attendee.email;
|
||||||
|
}),
|
||||||
|
notes: rawBooking.description || "",
|
||||||
|
location: {
|
||||||
|
value: rawBooking.location || "",
|
||||||
|
optionValue: rawBooking.location || "",
|
||||||
|
},
|
||||||
|
...responses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBooking(
|
||||||
|
prisma: PrismaClient,
|
||||||
|
uid: string,
|
||||||
|
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">
|
||||||
|
) {
|
||||||
|
const rawBooking = await prisma.booking.findFirst({
|
||||||
where: {
|
where: {
|
||||||
uid,
|
uid,
|
||||||
},
|
},
|
||||||
@@ -9,6 +58,7 @@ async function getBooking(prisma: PrismaClient, uid: string) {
|
|||||||
startTime: true,
|
startTime: true,
|
||||||
description: true,
|
description: true,
|
||||||
customInputs: true,
|
customInputs: true,
|
||||||
|
responses: true,
|
||||||
smsReminderNumber: true,
|
smsReminderNumber: true,
|
||||||
location: true,
|
location: true,
|
||||||
attendees: {
|
attendees: {
|
||||||
@@ -20,6 +70,14 @@ async function getBooking(prisma: PrismaClient, uid: string) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!rawBooking) {
|
||||||
|
return rawBooking;
|
||||||
|
}
|
||||||
|
|
||||||
|
const booking = getBookingWithResponses(rawBooking, {
|
||||||
|
bookingFields,
|
||||||
|
});
|
||||||
|
|
||||||
if (booking) {
|
if (booking) {
|
||||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||||
// probably fixable with json.stringify -> json.parse
|
// probably fixable with json.stringify -> json.parse
|
||||||
@@ -31,4 +89,23 @@ async function getBooking(prisma: PrismaClient, uid: string) {
|
|||||||
|
|
||||||
export type GetBookingType = Prisma.PromiseReturnType<typeof getBooking>;
|
export type GetBookingType = Prisma.PromiseReturnType<typeof getBooking>;
|
||||||
|
|
||||||
|
export const getBookingWithResponses = <
|
||||||
|
T extends Prisma.BookingGetPayload<{
|
||||||
|
select: BookingSelect & {
|
||||||
|
responses: true;
|
||||||
|
};
|
||||||
|
}>
|
||||||
|
>(
|
||||||
|
booking: T,
|
||||||
|
eventType: {
|
||||||
|
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
...booking,
|
||||||
|
responses: getBookingResponsesPartialSchema({
|
||||||
|
bookingFields: eventType.bookingFields,
|
||||||
|
}).parse(booking.responses || getResponsesFromOldBooking(booking)),
|
||||||
|
};
|
||||||
|
};
|
||||||
export default getBooking;
|
export default getBooking;
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export function isBrandingHidden(hideBrandingSetting: boolean, belongsToActiveTeam: boolean) {
|
export function isBrandingHidden(hideBrandingSetting: boolean, hasPaidPlan: boolean) {
|
||||||
return belongsToActiveTeam && hideBrandingSetting;
|
return hasPaidPlan && hideBrandingSetting;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import * as fetch from "@lib/core/http/fetch-wrapper";
|
import type { BookingCreateBody } from "@calcom/prisma/zod-utils";
|
||||||
import type { BookingCreateBody, BookingResponse } from "@lib/types/booking";
|
|
||||||
|
|
||||||
const createBooking = async (data: BookingCreateBody) => {
|
import * as fetch from "@lib/core/http/fetch-wrapper";
|
||||||
const response = await fetch.post<BookingCreateBody, BookingResponse>("/api/book/event", data);
|
import type { BookingResponse } from "@lib/types/booking";
|
||||||
|
|
||||||
|
type BookingCreateBodyForMutation = Omit<BookingCreateBody, "location">;
|
||||||
|
const createBooking = async (data: BookingCreateBodyForMutation) => {
|
||||||
|
const response = await fetch.post<BookingCreateBodyForMutation, BookingResponse>("/api/book/event", data);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import type { BookingCreateBody } from "@calcom/prisma/zod-utils";
|
||||||
import type { AppsStatus } from "@calcom/types/Calendar";
|
import type { AppsStatus } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import * as fetch from "@lib/core/http/fetch-wrapper";
|
import * as fetch from "@lib/core/http/fetch-wrapper";
|
||||||
import type { BookingCreateBody, BookingResponse } from "@lib/types/booking";
|
import type { BookingResponse } from "@lib/types/booking";
|
||||||
|
|
||||||
type ExtendedBookingCreateBody = BookingCreateBody & {
|
type ExtendedBookingCreateBody = BookingCreateBody & {
|
||||||
noEmail?: boolean;
|
noEmail?: boolean;
|
||||||
|
|||||||
@@ -2,36 +2,6 @@ import type { Attendee, Booking } from "@prisma/client";
|
|||||||
|
|
||||||
import type { AppsStatus } from "@calcom/types/Calendar";
|
import type { AppsStatus } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
export type BookingCreateBody = {
|
|
||||||
email: string;
|
|
||||||
end: string;
|
|
||||||
web3Details?: {
|
|
||||||
userWallet: string;
|
|
||||||
userSignature: unknown;
|
|
||||||
};
|
|
||||||
eventTypeId: number;
|
|
||||||
eventTypeSlug: string;
|
|
||||||
guests?: string[];
|
|
||||||
location: string;
|
|
||||||
name: string;
|
|
||||||
notes?: string;
|
|
||||||
rescheduleUid?: string;
|
|
||||||
recurringEventId?: string;
|
|
||||||
start: string;
|
|
||||||
timeZone: string;
|
|
||||||
user?: string | string[];
|
|
||||||
language: string;
|
|
||||||
bookingUid?: string;
|
|
||||||
customInputs: { label: string; value: string | boolean }[];
|
|
||||||
metadata: {
|
|
||||||
[key: string]: string;
|
|
||||||
};
|
|
||||||
hasHashedBookingLink: boolean;
|
|
||||||
hashedLink?: string | null;
|
|
||||||
smsReminderNumber?: string;
|
|
||||||
ethSignature?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BookingResponse = Booking & {
|
export type BookingResponse = Booking & {
|
||||||
paymentUid?: string;
|
paymentUid?: string;
|
||||||
attendees: Attendee[];
|
attendees: Attendee[];
|
||||||
|
|||||||
+25
-21
@@ -2,20 +2,6 @@ require("dotenv").config({ path: "../../.env" });
|
|||||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const { withSentryConfig } = require("@sentry/nextjs");
|
const { withSentryConfig } = require("@sentry/nextjs");
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
const withTM = require("next-transpile-modules")([
|
|
||||||
"@calcom/app-store",
|
|
||||||
"@calcom/core",
|
|
||||||
"@calcom/dayjs",
|
|
||||||
"@calcom/emails",
|
|
||||||
"@calcom/embed-core",
|
|
||||||
"@calcom/embed-react",
|
|
||||||
"@calcom/embed-snippet",
|
|
||||||
"@calcom/features",
|
|
||||||
"@calcom/lib",
|
|
||||||
"@calcom/prisma",
|
|
||||||
"@calcom/trpc",
|
|
||||||
"@calcom/ui",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { withAxiom } = require("next-axiom");
|
const { withAxiom } = require("next-axiom");
|
||||||
const { i18n } = require("./next-i18next.config");
|
const { i18n } = require("./next-i18next.config");
|
||||||
@@ -80,7 +66,6 @@ if (process.env.ANALYZE === "true") {
|
|||||||
plugins.push(withBundleAnalyzer);
|
plugins.push(withBundleAnalyzer);
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins.push(withTM);
|
|
||||||
plugins.push(withAxiom);
|
plugins.push(withAxiom);
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
@@ -94,12 +79,31 @@ const nextConfig = {
|
|||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: !!process.env.CI,
|
ignoreDuringBuilds: !!process.env.CI,
|
||||||
},
|
},
|
||||||
// TODO: We need to have all components in `@calcom/ui/components` in order to use this
|
transpilePackages: [
|
||||||
// modularizeImports: {
|
"@calcom/app-store",
|
||||||
// "@calcom/ui": {
|
"@calcom/core",
|
||||||
// transform: "@calcom/ui/components/{{member}}",
|
"@calcom/dayjs",
|
||||||
// },
|
"@calcom/emails",
|
||||||
// },
|
"@calcom/embed-core",
|
||||||
|
"@calcom/embed-react",
|
||||||
|
"@calcom/embed-snippet",
|
||||||
|
"@calcom/features",
|
||||||
|
"@calcom/lib",
|
||||||
|
"@calcom/prisma",
|
||||||
|
"@calcom/trpc",
|
||||||
|
"@calcom/ui",
|
||||||
|
],
|
||||||
|
modularizeImports: {
|
||||||
|
"@calcom/ui/components/icon": {
|
||||||
|
transform: "@react-icons/all-files/fi/{{member}}",
|
||||||
|
skipDefaultConversion: true,
|
||||||
|
preventFullImport: true,
|
||||||
|
},
|
||||||
|
// TODO: We need to have all components in `@calcom/ui/components` in order to use this
|
||||||
|
// "@calcom/ui": {
|
||||||
|
// transform: "@calcom/ui/components/{{member}}",
|
||||||
|
// },
|
||||||
|
},
|
||||||
images: {
|
images: {
|
||||||
unoptimized: true,
|
unoptimized: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.6.4",
|
"version": "2.6.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -45,7 +45,6 @@
|
|||||||
"@hookform/resolvers": "^2.9.7",
|
"@hookform/resolvers": "^2.9.7",
|
||||||
"@next-auth/prisma-adapter": "^1.0.4",
|
"@next-auth/prisma-adapter": "^1.0.4",
|
||||||
"@next/bundle-analyzer": "^13.1.6",
|
"@next/bundle-analyzer": "^13.1.6",
|
||||||
"@next/font": "^13.1.1",
|
|
||||||
"@radix-ui/react-avatar": "^1.0.0",
|
"@radix-ui/react-avatar": "^1.0.0",
|
||||||
"@radix-ui/react-collapsible": "^1.0.0",
|
"@radix-ui/react-collapsible": "^1.0.0",
|
||||||
"@radix-ui/react-dialog": "^1.0.0",
|
"@radix-ui/react-dialog": "^1.0.0",
|
||||||
@@ -70,12 +69,13 @@
|
|||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"dotenv-cli": "^6.0.0",
|
"dotenv-cli": "^6.0.0",
|
||||||
"eslint-config-next": "^13.1.6",
|
"eslint-config-next": "^13.2.1",
|
||||||
"googleapis": "^84.0.0",
|
"googleapis": "^84.0.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"ical.js": "^1.4.0",
|
"ical.js": "^1.4.0",
|
||||||
"ics": "^2.37.0",
|
"ics": "^2.37.0",
|
||||||
|
"jose": "^4.13.1",
|
||||||
"kbar": "^0.1.0-beta.36",
|
"kbar": "^0.1.0-beta.36",
|
||||||
"libphonenumber-js": "^1.10.12",
|
"libphonenumber-js": "^1.10.12",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -85,14 +85,13 @@
|
|||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"next": "^13.1.1",
|
"next": "^13.2.1",
|
||||||
"next-auth": "^4.18.8",
|
"next-auth": "^4.18.8",
|
||||||
"next-axiom": "^0.16.0",
|
"next-axiom": "^0.16.0",
|
||||||
"next-collect": "^0.2.1",
|
"next-collect": "^0.2.1",
|
||||||
"next-i18next": "^11.3.0",
|
"next-i18next": "^11.3.0",
|
||||||
"next-seo": "^4.26.0",
|
"next-seo": "^4.26.0",
|
||||||
"next-themes": "^0.2.0",
|
"next-themes": "^0.2.0",
|
||||||
"next-transpile-modules": "^10.0.0",
|
|
||||||
"nodemailer": "^6.7.8",
|
"nodemailer": "^6.7.8",
|
||||||
"otplib": "^12.0.1",
|
"otplib": "^12.0.1",
|
||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
@@ -104,7 +103,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-easy-crop": "^3.5.2",
|
"react-easy-crop": "^3.5.2",
|
||||||
"react-feather": "^2.0.10",
|
"react-feather": "^2.0.10",
|
||||||
"react-hook-form": "^7.34.2",
|
"react-hook-form": "^7.43.3",
|
||||||
"react-hot-toast": "^2.3.0",
|
"react-hot-toast": "^2.3.0",
|
||||||
"react-intl": "^5.25.1",
|
"react-intl": "^5.25.1",
|
||||||
"react-live-chat-loader": "^2.7.3",
|
"react-live-chat-loader": "^2.7.3",
|
||||||
@@ -116,9 +115,11 @@
|
|||||||
"react-use-intercom": "1.5.1",
|
"react-use-intercom": "1.5.1",
|
||||||
"react-virtualized-auto-sizer": "^1.0.6",
|
"react-virtualized-auto-sizer": "^1.0.6",
|
||||||
"react-window": "^1.8.7",
|
"react-window": "^1.8.7",
|
||||||
|
"remark": "^14.0.2",
|
||||||
"rrule": "^2.7.1",
|
"rrule": "^2.7.1",
|
||||||
"schema-dts": "^1.1.0",
|
"schema-dts": "^1.1.0",
|
||||||
"short-uuid": "^4.2.0",
|
"short-uuid": "^4.2.0",
|
||||||
|
"strip-markdown": "^5.0.0",
|
||||||
"stripe": "^9.16.0",
|
"stripe": "^9.16.0",
|
||||||
"superjson": "1.9.1",
|
"superjson": "1.9.1",
|
||||||
"tailwindcss-radix": "^2.6.0",
|
"tailwindcss-radix": "^2.6.0",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { BadgeCheckIcon } from "@heroicons/react/solid";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import MarkdownIt from "markdown-it";
|
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -23,20 +23,19 @@ import defaultEvents, {
|
|||||||
} from "@calcom/lib/defaultEvents";
|
} from "@calcom/lib/defaultEvents";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { HeadSeo, AvatarGroup, Avatar } from "@calcom/ui";
|
import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui";
|
||||||
import { BadgeCheckIcon, FiArrowRight } from "@calcom/ui/components/icon";
|
import { FiArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
|
||||||
|
|
||||||
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
|
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
|
||||||
const { users, profile, eventTypes, isDynamicGroup, dynamicNames, dynamicUsernames, isSingleUser } = props;
|
const { users, profile, eventTypes, isDynamicGroup, dynamicNames, dynamicUsernames, isSingleUser } = props;
|
||||||
const [user] = users; //To be used when we only have a single user, not dynamic group
|
const [user] = users; //To be used when we only have a single user, not dynamic group
|
||||||
@@ -146,7 +145,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
{!isBioEmpty && (
|
{!isBioEmpty && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
className=" dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||||
dangerouslySetInnerHTML={{ __html: md.render(user.bio || "") }}
|
dangerouslySetInnerHTML={{ __html: md.render(user.bio || "") }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import type { LocationObject } from "@calcom/app-store/locations";
|
import type { LocationObject } from "@calcom/app-store/locations";
|
||||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { addListFormatting } from "@calcom/lib/markdownIt";
|
||||||
import type { User } from "@calcom/prisma/client";
|
import type { User } from "@calcom/prisma/client";
|
||||||
|
|
||||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||||
@@ -69,7 +71,8 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
username,
|
/** TODO: We should standarize this */
|
||||||
|
username: username.toLowerCase().replace(/( |%20)/g, "+"),
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -82,6 +85,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
weekStart: true,
|
weekStart: true,
|
||||||
brandColor: true,
|
brandColor: true,
|
||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
|
metadata: true,
|
||||||
eventTypes: {
|
eventTypes: {
|
||||||
where: {
|
where: {
|
||||||
// Many-to-many relationship causes inclusion of the team events - cool -
|
// Many-to-many relationship causes inclusion of the team events - cool -
|
||||||
@@ -121,13 +125,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const md = new MarkdownIt("zero").enable([
|
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||||
//
|
|
||||||
"emphasis",
|
|
||||||
"list",
|
|
||||||
"newline",
|
|
||||||
"strikethrough",
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!user || !user.eventTypes.length) return { notFound: true };
|
if (!user || !user.eventTypes.length) return { notFound: true };
|
||||||
|
|
||||||
@@ -155,9 +153,9 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
||||||
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
|
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
|
||||||
locations: privacyFilteredLocations(locations),
|
locations: privacyFilteredLocations(locations),
|
||||||
descriptionAsSafeHTML: eventType.description ? md.render(eventType.description) : null,
|
descriptionAsSafeHTML: eventType.description ? addListFormatting(md.render(eventType.description)) : null,
|
||||||
});
|
});
|
||||||
// Check if the user you are logging into has any active teams
|
// Check if the user you are logging into has any active teams or premium user name
|
||||||
const hasActiveTeam =
|
const hasActiveTeam =
|
||||||
user.teams.filter((m) => {
|
user.teams.filter((m) => {
|
||||||
if (!IS_TEAM_BILLING_ENABLED) return true;
|
if (!IS_TEAM_BILLING_ENABLED) return true;
|
||||||
@@ -166,6 +164,8 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
return false;
|
return false;
|
||||||
}).length > 0;
|
}).length > 0;
|
||||||
|
|
||||||
|
const hasPremiumUserName = hasKeyInMetadata(user, "isPremium");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
@@ -182,7 +182,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
away: user?.away,
|
away: user?.away,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssg.dehydrate(),
|
||||||
isBrandingHidden: isBrandingHidden(user.hideBranding, hasActiveTeam),
|
isBrandingHidden: isBrandingHidden(user.hideBranding, hasActiveTeam || hasPremiumUserName),
|
||||||
},
|
},
|
||||||
revalidate: 10, // seconds
|
revalidate: 10, // seconds
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { GetServerSidePropsContext } from "next";
|
|||||||
import type { LocationObject } from "@calcom/app-store/locations";
|
import type { LocationObject } from "@calcom/app-store/locations";
|
||||||
import { privacyFilteredLocations } from "@calcom/app-store/locations";
|
import { privacyFilteredLocations } from "@calcom/app-store/locations";
|
||||||
import { getAppFromSlug } from "@calcom/app-store/utils";
|
import { getAppFromSlug } from "@calcom/app-store/utils";
|
||||||
|
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import {
|
import {
|
||||||
getDefaultEvent,
|
getDefaultEvent,
|
||||||
@@ -118,10 +119,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!eventTypeRaw) return { notFound: true };
|
if (!eventTypeRaw) return { notFound: true };
|
||||||
|
|
||||||
const eventType = {
|
const eventType = {
|
||||||
...eventTypeRaw,
|
...eventTypeRaw,
|
||||||
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}),
|
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}),
|
||||||
|
bookingFields: getBookingFieldsWithSystemFields(eventTypeRaw),
|
||||||
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -183,7 +184,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
prisma,
|
prisma,
|
||||||
context.query.rescheduleUid
|
context.query.rescheduleUid
|
||||||
? (context.query.rescheduleUid as string)
|
? (context.query.rescheduleUid as string)
|
||||||
: (context.query.bookingUid as string)
|
: (context.query.bookingUid as string),
|
||||||
|
eventTypeObject.bookingFields
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Inter } from "@next/font/google";
|
|
||||||
import localFont from "@next/font/local";
|
|
||||||
import { DefaultSeo } from "next-seo";
|
import { DefaultSeo } from "next-seo";
|
||||||
|
import { Inter } from "next/font/google";
|
||||||
|
import localFont from "next/font/local";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { UserPermissionRole } from "@prisma/client";
|
|||||||
import { IdentityProvider } from "@prisma/client";
|
import { IdentityProvider } from "@prisma/client";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import Handlebars from "handlebars";
|
import Handlebars from "handlebars";
|
||||||
|
import { SignJWT } from "jose";
|
||||||
import type { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
import NextAuth from "next-auth";
|
import NextAuth from "next-auth";
|
||||||
import { encode } from "next-auth/jwt";
|
import { encode } from "next-auth/jwt";
|
||||||
@@ -18,7 +19,7 @@ import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
|||||||
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
|
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
|
||||||
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||||
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
|
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
|
||||||
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
import { defaultCookies } from "@calcom/lib/default-cookies";
|
import { defaultCookies } from "@calcom/lib/default-cookies";
|
||||||
import { randomString } from "@calcom/lib/random";
|
import { randomString } from "@calcom/lib/random";
|
||||||
@@ -38,6 +39,21 @@ const transporter = nodemailer.createTransport<TransportOptions>({
|
|||||||
|
|
||||||
const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase();
|
const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase();
|
||||||
|
|
||||||
|
const signJwt = async (payload: { email: string }) => {
|
||||||
|
const secret = new TextEncoder().encode(process.env.CALENDSO_ENCRYPTION_KEY);
|
||||||
|
return new SignJWT(payload)
|
||||||
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
|
.setSubject(payload.email)
|
||||||
|
.setIssuedAt()
|
||||||
|
.setIssuer(WEBSITE_URL)
|
||||||
|
.setAudience(`${WEBSITE_URL}/auth/login`)
|
||||||
|
.setExpirationTime("2m")
|
||||||
|
.sign(secret);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginWithTotp = async (user: { email: string }) =>
|
||||||
|
`/auth/login?totp=${await signJwt({ email: user.email })}`;
|
||||||
|
|
||||||
const providers: Provider[] = [
|
const providers: Provider[] = [
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
id: "credentials",
|
id: "credentials",
|
||||||
@@ -82,17 +98,19 @@ const providers: Provider[] = [
|
|||||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.identityProvider !== IdentityProvider.CAL) {
|
if (user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
|
||||||
throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled);
|
throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.password) {
|
if (!user.password && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
|
||||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
|
if (user.password) {
|
||||||
if (!isCorrectPassword) {
|
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
|
||||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
if (!isCorrectPassword) {
|
||||||
|
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.twoFactorEnabled) {
|
if (user.twoFactorEnabled) {
|
||||||
@@ -130,7 +148,7 @@ const providers: Provider[] = [
|
|||||||
await limiter.check(10, user.email); // 10 requests per minute
|
await limiter.check(10, user.email); // 10 requests per minute
|
||||||
// Check if the user you are logging into has any active teams
|
// Check if the user you are logging into has any active teams
|
||||||
const hasActiveTeams =
|
const hasActiveTeams =
|
||||||
user.teams.filter((m) => {
|
user.teams.filter((m: { team: { metadata: unknown } }) => {
|
||||||
if (!IS_TEAM_BILLING_ENABLED) return true;
|
if (!IS_TEAM_BILLING_ENABLED) return true;
|
||||||
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
|
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
|
||||||
if (metadata.success && metadata.data?.subscriptionId) return true;
|
if (metadata.success && metadata.data?.subscriptionId) return true;
|
||||||
@@ -449,7 +467,11 @@ export default NextAuth({
|
|||||||
console.error("Error while linking account of already existing user");
|
console.error("Error while linking account of already existing user");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
if (existingUser.twoFactorEnabled) {
|
||||||
|
return loginWithTotp(existingUser);
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the email address doesn't match, check if an account already exists
|
// If the email address doesn't match, check if an account already exists
|
||||||
@@ -461,7 +483,11 @@ export default NextAuth({
|
|||||||
|
|
||||||
if (!userWithNewEmail) {
|
if (!userWithNewEmail) {
|
||||||
await prisma.user.update({ where: { id: existingUser.id }, data: { email: user.email } });
|
await prisma.user.update({ where: { id: existingUser.id }, data: { email: user.email } });
|
||||||
return true;
|
if (existingUser.twoFactorEnabled) {
|
||||||
|
return loginWithTotp(existingUser);
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return "/auth/error?error=new-email-conflict";
|
return "/auth/error?error=new-email-conflict";
|
||||||
}
|
}
|
||||||
@@ -477,7 +503,11 @@ export default NextAuth({
|
|||||||
if (existingUserWithEmail) {
|
if (existingUserWithEmail) {
|
||||||
// if self-hosted then we can allow auto-merge of identity providers if email is verified
|
// if self-hosted then we can allow auto-merge of identity providers if email is verified
|
||||||
if (!hostedCal && existingUserWithEmail.emailVerified) {
|
if (!hostedCal && existingUserWithEmail.emailVerified) {
|
||||||
return true;
|
if (existingUserWithEmail.twoFactorEnabled) {
|
||||||
|
return loginWithTotp(existingUserWithEmail);
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if user was invited
|
// check if user was invited
|
||||||
@@ -499,7 +529,11 @@ export default NextAuth({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
if (existingUserWithEmail.twoFactorEnabled) {
|
||||||
|
return loginWithTotp(existingUserWithEmail);
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// User signs up with email/password and then tries to login with Google/SAML using the same email
|
// User signs up with email/password and then tries to login with Google/SAML using the same email
|
||||||
@@ -511,7 +545,11 @@ export default NextAuth({
|
|||||||
where: { email: existingUserWithEmail.email },
|
where: { email: existingUserWithEmail.email },
|
||||||
data: { password: null },
|
data: { password: null },
|
||||||
});
|
});
|
||||||
return true;
|
if (existingUserWithEmail.twoFactorEnabled) {
|
||||||
|
return loginWithTotp(existingUserWithEmail);
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} else if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) {
|
} else if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) {
|
||||||
return "/auth/error?error=use-password-login";
|
return "/auth/error?error=use-password-login";
|
||||||
}
|
}
|
||||||
@@ -534,7 +572,11 @@ export default NextAuth({
|
|||||||
const linkAccountNewUserData = { ...account, userId: newUser.id };
|
const linkAccountNewUserData = { ...account, userId: newUser.id };
|
||||||
await calcomAdapter.linkAccount(linkAccountNewUserData);
|
await calcomAdapter.linkAccount(linkAccountNewUserData);
|
||||||
|
|
||||||
return true;
|
if (account.twoFactorEnabled) {
|
||||||
|
return loginWithTotp(newUser);
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
// get user's credentials + their connected integrations
|
// get user's credentials + their connected integrations
|
||||||
const calendarCredentials = getCalendarCredentials(user.credentials);
|
const calendarCredentials = getCalendarCredentials(user.credentials);
|
||||||
// get all the connected integrations' calendars (from third party)
|
// get all the connected integrations' calendars (from third party)
|
||||||
const connectedCalendars = await getConnectedCalendars(calendarCredentials, user.selectedCalendars);
|
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, user.selectedCalendars);
|
||||||
const calendars = connectedCalendars.flatMap((c) => c.calendars).filter(notEmpty);
|
const calendars = connectedCalendars.flatMap((c) => c.calendars).filter(notEmpty);
|
||||||
const selectableCalendars = calendars.map((cal) => {
|
const selectableCalendars = calendars.map((cal) => {
|
||||||
return { selected: selectedCalendarIds.findIndex((s) => s.externalId === cal.externalId) > -1, ...cal };
|
return { selected: selectedCalendarIds.findIndex((s) => s.externalId === cal.externalId) > -1, ...cal };
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ async function handler(req: NextApiRequest & { userId?: number }) {
|
|||||||
const session = await getSession({ req });
|
const session = await getSession({ req });
|
||||||
/* To mimic API behavior and comply with types */
|
/* To mimic API behavior and comply with types */
|
||||||
req.userId = session?.user?.id || -1;
|
req.userId = session?.user?.id || -1;
|
||||||
const booking = await handleNewBooking(req);
|
const booking = await handleNewBooking(req, {
|
||||||
|
isNotAnApiCall: true,
|
||||||
|
});
|
||||||
return booking;
|
return booking;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
runtime: "edge",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function handler(req: NextRequest) {
|
||||||
|
const countryCode = req.headers.get("x-vercel-ip-country") ?? "";
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
countryCode,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { fetcher } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
|
|
||||||
import { getSession } from "@calcom/lib/auth";
|
|
||||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
|
||||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
|
||||||
|
|
||||||
const getAccessLinkSchema = z.object({
|
|
||||||
download_link: z.string().url(),
|
|
||||||
// expires (timestamp), s3_bucket, s3_region, s3_key
|
|
||||||
});
|
|
||||||
|
|
||||||
const requestQuery = z.object({
|
|
||||||
recordingId: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const isDownloadAllowed = async (req: NextApiRequest) => {
|
|
||||||
if (IS_SELF_HOSTED) return true;
|
|
||||||
const session = await getSession({ req });
|
|
||||||
// Check if user belong to active team
|
|
||||||
return !session?.user?.belongsToActiveTeam;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<z.infer<typeof getAccessLinkSchema> | void>
|
|
||||||
) {
|
|
||||||
const { recordingId } = requestQuery.parse(req.query);
|
|
||||||
|
|
||||||
if (!(await isDownloadAllowed(req))) return res.status(403);
|
|
||||||
|
|
||||||
const response = await fetcher(`/recordings/${recordingId}/access-link`).then(getAccessLinkSchema.parse);
|
|
||||||
return res.status(200).json(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defaultHandler({
|
|
||||||
GET: Promise.resolve({ default: defaultResponder(handler) }),
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ImageResponse } from "@vercel/og";
|
import { ImageResponse } from "@vercel/og";
|
||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
import { remark } from "remark";
|
||||||
import type { SatoriOptions } from "satori";
|
import type { SatoriOptions } from "satori";
|
||||||
|
import strip from "strip-markdown";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Meeting, App, Generic } from "@calcom/lib/OgImages";
|
import { Meeting, App, Generic } from "@calcom/lib/OgImages";
|
||||||
@@ -18,7 +20,7 @@ const interFontMedium = fetch(new URL("../../../../public/fonts/Inter-Medium.ttf
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
runtime: "experimental-edge",
|
runtime: "edge",
|
||||||
};
|
};
|
||||||
|
|
||||||
const meetingSchema = z.object({
|
const meetingSchema = z.object({
|
||||||
@@ -106,8 +108,8 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
description: searchParams.get("description"),
|
description: searchParams.get("description"),
|
||||||
imageType,
|
imageType,
|
||||||
});
|
});
|
||||||
|
const description_ = await (await remark().use(strip).process(description)).toString();
|
||||||
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig) as {
|
const img = new ImageResponse(<Generic title={title} description={description_} />, ogConfig) as {
|
||||||
body: Buffer;
|
body: Buffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export default trpcNext.createNextApiHandler({
|
|||||||
"viewer.public.i18n": `no-cache`,
|
"viewer.public.i18n": `no-cache`,
|
||||||
// Revalidation time here should be 1 second, per https://github.com/calcom/cal.com/pull/6823#issuecomment-1423215321
|
// Revalidation time here should be 1 second, per https://github.com/calcom/cal.com/pull/6823#issuecomment-1423215321
|
||||||
"viewer.public.slots.getSchedule": `no-cache`, // FIXME
|
"viewer.public.slots.getSchedule": `no-cache`, // FIXME
|
||||||
|
"viewer.public.cityTimezones": `max-age=${ONE_DAY_IN_SECONDS}, stale-while-revalidate`,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Find which element above is an exact match for this group of paths
|
// Find which element above is an exact match for this group of paths
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useReducer, useState } from "react";
|
import { useCallback, useReducer, useState } from "react";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
import { AppSettings } from "@calcom/app-store/_components/AppSettings";
|
import { AppSettings } from "@calcom/app-store/_components/AppSettings";
|
||||||
@@ -7,8 +7,9 @@ import { InstallAppButton } from "@calcom/app-store/components";
|
|||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
|
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
|
||||||
import { InstalledAppVariants } from "@calcom/app-store/utils";
|
import { InstalledAppVariants } from "@calcom/app-store/utils";
|
||||||
import { AppSetDefaultLinkDailog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
|
import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
|
||||||
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
||||||
|
import { BulkEditDefaultConferencingModal } from "@calcom/features/eventtypes/components/BulkEditDefaultConferencingModal";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -113,11 +114,16 @@ interface IntegrationsListProps {
|
|||||||
const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListProps) => {
|
const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListProps) => {
|
||||||
const { data: defaultConferencingApp } = trpc.viewer.getUsersDefaultConferencingApp.useQuery();
|
const { data: defaultConferencingApp } = trpc.viewer.getUsersDefaultConferencingApp.useQuery();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
|
||||||
const [locationType, setLocationType] = useState<(EventLocationType & { slug: string }) | undefined>(
|
const [locationType, setLocationType] = useState<(EventLocationType & { slug: string }) | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onSuccessCallback = useCallback(() => {
|
||||||
|
setBulkUpdateModal(true);
|
||||||
|
showToast("Default app updated successfully", "success");
|
||||||
|
}, []);
|
||||||
|
|
||||||
const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({
|
const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showToast("Default app updated successfully", "success");
|
showToast("Default app updated successfully", "success");
|
||||||
@@ -172,6 +178,7 @@ const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListP
|
|||||||
updateDefaultAppMutation.mutate({
|
updateDefaultAppMutation.mutate({
|
||||||
appSlug,
|
appSlug,
|
||||||
});
|
});
|
||||||
|
setBulkUpdateModal(true);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
{t("change_default_conferencing_app")}
|
{t("change_default_conferencing_app")}
|
||||||
@@ -196,11 +203,16 @@ const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListP
|
|||||||
})}
|
})}
|
||||||
</List>
|
</List>
|
||||||
{locationType && (
|
{locationType && (
|
||||||
<AppSetDefaultLinkDailog
|
<AppSetDefaultLinkDialog
|
||||||
locationType={locationType}
|
locationType={locationType}
|
||||||
setLocationType={() => setLocationType(undefined)}
|
setLocationType={() => setLocationType(undefined)}
|
||||||
|
onSuccess={onSuccessCallback}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{bulkUpdateModal && (
|
||||||
|
<BulkEditDefaultConferencingModal open={bulkUpdateModal} setOpen={setBulkUpdateModal} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { jwtVerify } from "jose";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import { getCsrfToken, signIn } from "next-auth/react";
|
import { getCsrfToken, signIn } from "next-auth/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -42,14 +43,14 @@ export default function Login({
|
|||||||
isSAMLLoginEnabled,
|
isSAMLLoginEnabled,
|
||||||
samlTenantID,
|
samlTenantID,
|
||||||
samlProductID,
|
samlProductID,
|
||||||
|
totpEmail,
|
||||||
}: inferSSRProps<typeof _getServerSideProps> & WithNonceProps) {
|
}: inferSSRProps<typeof _getServerSideProps> & WithNonceProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const methods = useForm<LoginValues>();
|
const methods = useForm<LoginValues>();
|
||||||
|
|
||||||
const { register, formState } = methods;
|
const { register, formState } = methods;
|
||||||
|
const [twoFactorRequired, setTwoFactorRequired] = useState(!!totpEmail || false);
|
||||||
const [twoFactorRequired, setTwoFactorRequired] = useState(false);
|
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
const errorMessages: { [key: string]: string } = {
|
const errorMessages: { [key: string]: string } = {
|
||||||
@@ -94,6 +95,16 @@ export default function Login({
|
|||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ExternalTotpFooter = (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
window.location.replace("/");
|
||||||
|
}}
|
||||||
|
color="minimal">
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
const onSubmit = async (values: LoginValues) => {
|
const onSubmit = async (values: LoginValues) => {
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
telemetry.event(telemetryEventTypes.login, collectPageParameters());
|
telemetry.event(telemetryEventTypes.login, collectPageParameters());
|
||||||
@@ -120,7 +131,9 @@ export default function Login({
|
|||||||
heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")}
|
heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")}
|
||||||
footerText={
|
footerText={
|
||||||
twoFactorRequired
|
twoFactorRequired
|
||||||
? TwoFactorFooter
|
? !totpEmail
|
||||||
|
? TwoFactorFooter
|
||||||
|
: ExternalTotpFooter
|
||||||
: process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== "true"
|
: process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== "true"
|
||||||
? LoginFooter
|
? LoginFooter
|
||||||
: null
|
: null
|
||||||
@@ -135,7 +148,7 @@ export default function Login({
|
|||||||
<EmailField
|
<EmailField
|
||||||
id="email"
|
id="email"
|
||||||
label={t("email_address")}
|
label={t("email_address")}
|
||||||
defaultValue={router.query.email as string}
|
defaultValue={totpEmail || (router.query.email as string)}
|
||||||
placeholder="john.doe@example.com"
|
placeholder="john.doe@example.com"
|
||||||
required
|
required
|
||||||
{...register("email")}
|
{...register("email")}
|
||||||
@@ -152,7 +165,7 @@ export default function Login({
|
|||||||
<PasswordField
|
<PasswordField
|
||||||
id="password"
|
id="password"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
required
|
required={!totpEmail}
|
||||||
className="mb-0"
|
className="mb-0"
|
||||||
{...register("password")}
|
{...register("password")}
|
||||||
/>
|
/>
|
||||||
@@ -211,6 +224,40 @@ const _getServerSideProps = async function getServerSideProps(context: GetServer
|
|||||||
const session = await getSession({ req });
|
const session = await getSession({ req });
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
const verifyJwt = (jwt: string) => {
|
||||||
|
const secret = new TextEncoder().encode(process.env.CALENDSO_ENCRYPTION_KEY);
|
||||||
|
|
||||||
|
return jwtVerify(jwt, secret, {
|
||||||
|
issuer: WEBSITE_URL,
|
||||||
|
audience: `${WEBSITE_URL}/auth/login`,
|
||||||
|
algorithms: ["HS256"],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let totpEmail = null;
|
||||||
|
if (context.query.totp) {
|
||||||
|
try {
|
||||||
|
const decryptedJwt = await verifyJwt(context.query.totp as string);
|
||||||
|
if (decryptedJwt.payload) {
|
||||||
|
totpEmail = decryptedJwt.payload.email as string;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/auth/error?error=JWT%20Invalid%20Payload",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/auth/error?error=Invalid%20JWT%3A%20Please%20try%20again",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -238,6 +285,7 @@ const _getServerSideProps = async function getServerSideProps(context: GetServer
|
|||||||
isSAMLLoginEnabled,
|
isSAMLLoginEnabled,
|
||||||
samlTenantID,
|
samlTenantID,
|
||||||
samlProductID,
|
samlProductID,
|
||||||
|
totpEmail,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
import { useRouter } from "next/router";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ import Shell from "@calcom/features/shell/Shell";
|
|||||||
import { availabilityAsString } from "@calcom/lib/availability";
|
import { availabilityAsString } from "@calcom/lib/availability";
|
||||||
import { yyyymmdd } from "@calcom/lib/date-fns";
|
import { yyyymmdd } from "@calcom/lib/date-fns";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||||
import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/types/schedule";
|
import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/types/schedule";
|
||||||
@@ -22,19 +22,20 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
TimezoneSelect,
|
TimezoneSelect,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
ConfirmationDialogContent,
|
||||||
VerticalDivider,
|
VerticalDivider,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiInfo, FiPlus } from "@calcom/ui/components/icon";
|
import { FiInfo, FiPlus, FiTrash } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
|
||||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||||
import EditableHeading from "@components/ui/EditableHeading";
|
import EditableHeading from "@components/ui/EditableHeading";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
schedule: stringOrNumber,
|
schedule: z.coerce.number().positive().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AvailabilityFormValues = {
|
type AvailabilityFormValues = {
|
||||||
@@ -84,15 +85,29 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Availability({ schedule }: { schedule: number }) {
|
export default function Availability() {
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const me = useMeQuery();
|
const me = useMeQuery();
|
||||||
|
const {
|
||||||
|
data: { schedule: scheduleId },
|
||||||
|
} = useTypedQuery(querySchema);
|
||||||
|
|
||||||
const { timeFormat } = me.data || { timeFormat: null };
|
const { timeFormat } = me.data || { timeFormat: null };
|
||||||
const { data, isLoading } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId: schedule });
|
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(
|
||||||
const { data: defaultValues } = trpc.viewer.availability.defaultValues.useQuery({ scheduleId: schedule });
|
{ scheduleId },
|
||||||
const form = useForm<AvailabilityFormValues>({ defaultValues });
|
{
|
||||||
const { control } = form;
|
enabled: !!scheduleId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useForm<AvailabilityFormValues>({
|
||||||
|
values: schedule && {
|
||||||
|
...schedule,
|
||||||
|
schedule: schedule?.availability || [],
|
||||||
|
},
|
||||||
|
});
|
||||||
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
|
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
|
||||||
onSuccess: async ({ prevDefaultId, currentDefaultId, ...data }) => {
|
onSuccess: async ({ prevDefaultId, currentDefaultId, ...data }) => {
|
||||||
if (prevDefaultId && currentDefaultId) {
|
if (prevDefaultId && currentDefaultId) {
|
||||||
@@ -120,10 +135,26 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const deleteMutation = trpc.viewer.availability.schedule.delete.useMutation({
|
||||||
|
onError: (err) => {
|
||||||
|
if (err instanceof HttpError) {
|
||||||
|
const message = `${err.statusCode}: ${err.message}`;
|
||||||
|
showToast(message, "error");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
utils.viewer.availability.list.invalidate();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast(t("schedule_deleted_successfully"), "success");
|
||||||
|
router.push("/availability");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell
|
<Shell
|
||||||
backPath="/availability"
|
backPath="/availability"
|
||||||
title={data?.schedule.name ? data.schedule.name + " | " + t("availability") : t("availability")}
|
title={schedule?.name ? schedule.name + " | " + t("availability") : t("availability")}
|
||||||
heading={
|
heading={
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -134,8 +165,8 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
subtitle={
|
subtitle={
|
||||||
data ? (
|
schedule ? (
|
||||||
data.schedule.availability
|
schedule.schedule
|
||||||
.filter((availability) => !!availability.days.length)
|
.filter((availability) => !!availability.days.length)
|
||||||
.map((availability) => (
|
.map((availability) => (
|
||||||
<span key={availability.id}>
|
<span key={availability.id}>
|
||||||
@@ -158,7 +189,7 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
</Skeleton>
|
</Skeleton>
|
||||||
<Switch
|
<Switch
|
||||||
id="hiddenSwitch"
|
id="hiddenSwitch"
|
||||||
disabled={isLoading || data?.isDefault}
|
disabled={isLoading || schedule?.isDefault}
|
||||||
checked={form.watch("isDefault")}
|
checked={form.watch("isDefault")}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
form.setValue("isDefault", e);
|
form.setValue("isDefault", e);
|
||||||
@@ -166,6 +197,24 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<VerticalDivider />
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button StartIcon={FiTrash} variant="icon" color="destructive" aria-label={t("delete")} />
|
||||||
|
</DialogTrigger>
|
||||||
|
<ConfirmationDialogContent
|
||||||
|
isLoading={deleteMutation.isLoading}
|
||||||
|
variety="danger"
|
||||||
|
title={t("delete_schedule")}
|
||||||
|
confirmBtnText={t("delete")}
|
||||||
|
loadingText={t("delete")}
|
||||||
|
onConfirm={() => {
|
||||||
|
scheduleId && deleteMutation.mutate({ scheduleId });
|
||||||
|
}}>
|
||||||
|
{t("delete_schedule_description")}
|
||||||
|
</ConfirmationDialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<VerticalDivider />
|
<VerticalDivider />
|
||||||
|
|
||||||
<div className="border-l-2 border-gray-300" />
|
<div className="border-l-2 border-gray-300" />
|
||||||
@@ -179,11 +228,12 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
form={form}
|
form={form}
|
||||||
id="availability-form"
|
id="availability-form"
|
||||||
handleSubmit={async ({ dateOverrides, ...values }) => {
|
handleSubmit={async ({ dateOverrides, ...values }) => {
|
||||||
updateMutation.mutate({
|
scheduleId &&
|
||||||
scheduleId: schedule,
|
updateMutation.mutate({
|
||||||
dateOverrides: dateOverrides.flatMap((override) => override.ranges),
|
scheduleId,
|
||||||
...values,
|
dateOverrides: dateOverrides.flatMap((override) => override.ranges),
|
||||||
});
|
...values,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="flex flex-col sm:mx-0 xl:flex-row xl:space-x-6">
|
className="flex flex-col sm:mx-0 xl:flex-row xl:space-x-6">
|
||||||
<div className="flex-1 flex-row xl:mr-0">
|
<div className="flex-1 flex-row xl:mr-0">
|
||||||
@@ -191,7 +241,7 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
<div>
|
<div>
|
||||||
{typeof me.data?.weekStart === "string" && (
|
{typeof me.data?.weekStart === "string" && (
|
||||||
<Schedule
|
<Schedule
|
||||||
control={control}
|
control={form.control}
|
||||||
name="schedule"
|
name="schedule"
|
||||||
weekStart={
|
weekStart={
|
||||||
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
|
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
|
||||||
@@ -203,7 +253,7 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="my-6 rounded-md border">
|
<div className="my-6 rounded-md border">
|
||||||
{data?.workingHours && <DateOverride workingHours={data.workingHours} />}
|
{schedule?.workingHours && <DateOverride workingHours={schedule.workingHours} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
|
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
|
||||||
@@ -243,20 +293,3 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
</Shell>
|
</Shell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|
||||||
const params = querySchema.safeParse(ctx.params);
|
|
||||||
const ssr = await ssrInit(ctx);
|
|
||||||
|
|
||||||
if (!params.success) return { notFound: true };
|
|
||||||
|
|
||||||
const scheduleId = params.data.schedule;
|
|
||||||
await ssr.viewer.availability.schedule.get.fetch({ scheduleId });
|
|
||||||
await ssr.viewer.availability.defaultValues.fetch({ scheduleId });
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
schedule: scheduleId,
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
|
|
||||||
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
@@ -14,8 +13,6 @@ import { HttpError } from "@lib/core/http/error";
|
|||||||
|
|
||||||
import SkeletonLoader from "@components/availability/SkeletonLoader";
|
import SkeletonLoader from "@components/availability/SkeletonLoader";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
|
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
@@ -118,13 +115,3 @@ export default function AvailabilityPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BookingStatus, WorkflowActions } from "@prisma/client";
|
import { BookingStatus } from "@prisma/client";
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { createEvent } from "ics";
|
import { createEvent } from "ics";
|
||||||
@@ -23,6 +23,10 @@ import {
|
|||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
|
import {
|
||||||
|
SystemField,
|
||||||
|
getBookingFieldsWithSystemFields,
|
||||||
|
} from "@calcom/features/bookings/lib/getBookingFields";
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
@@ -42,10 +46,11 @@ import prisma from "@calcom/prisma";
|
|||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Button, EmailInput, HeadSeo } from "@calcom/ui";
|
import { Badge, Button, EmailInput, HeadSeo } from "@calcom/ui";
|
||||||
import { FiX, FiExternalLink, FiChevronLeft, FiCheck, FiCalendar } from "@calcom/ui/components/icon";
|
import { FiX, FiExternalLink, FiChevronLeft, FiCheck, FiCalendar } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
|
import { getBookingWithResponses } from "@lib/getBooking";
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import CancelBooking from "@components/booking/CancelBooking";
|
import CancelBooking from "@components/booking/CancelBooking";
|
||||||
@@ -195,10 +200,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
window.scrollTo(0, document.body.scrollHeight);
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
const location: ReturnType<typeof getEventLocationValue> = props.bookingInfo.location
|
const location = props.bookingInfo.location as ReturnType<typeof getEventLocationValue>;
|
||||||
? props.bookingInfo.location
|
|
||||||
: // If there is no location set then we default to Cal Video
|
|
||||||
"integrations:daily";
|
|
||||||
|
|
||||||
const locationVideoCallUrl: string | undefined = bookingMetadataSchema.parse(
|
const locationVideoCallUrl: string | undefined = bookingMetadataSchema.parse(
|
||||||
props?.bookingInfo?.metadata || {}
|
props?.bookingInfo?.metadata || {}
|
||||||
@@ -335,12 +337,12 @@ export default function Success(props: SuccessProps) {
|
|||||||
}
|
}
|
||||||
return t("emailed_you_and_attendees" + titleSuffix);
|
return t("emailed_you_and_attendees" + titleSuffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
|
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
|
||||||
useTheme(isSuccessBookingPage ? props.profile.theme : "light");
|
useTheme(isSuccessBookingPage ? props.profile.theme : "light");
|
||||||
const title = t(
|
const title = t(
|
||||||
`booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}`
|
`booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}`
|
||||||
);
|
);
|
||||||
const customInputs = bookingInfo?.customInputs;
|
|
||||||
|
|
||||||
const locationToDisplay = getSuccessPageLocationMessage(
|
const locationToDisplay = getSuccessPageLocationMessage(
|
||||||
locationVideoCallUrl ? locationVideoCallUrl : location,
|
locationVideoCallUrl ? locationVideoCallUrl : location,
|
||||||
@@ -348,10 +350,6 @@ export default function Success(props: SuccessProps) {
|
|||||||
bookingInfo.status
|
bookingInfo.status
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasSMSAttendeeAction =
|
|
||||||
eventType.workflows.find((workflowEventType) =>
|
|
||||||
workflowEventType.workflow.steps.find((step) => step.action === WorkflowActions.SMS_ATTENDEE)
|
|
||||||
) !== undefined;
|
|
||||||
const providerName = guessEventLocationType(location)?.label;
|
const providerName = guessEventLocationType(location)?.label;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -442,17 +440,19 @@ export default function Success(props: SuccessProps) {
|
|||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<p className="text-gray-600 dark:text-gray-300">{getTitle()}</p>
|
<p className="text-gray-600 dark:text-gray-300">{getTitle()}</p>
|
||||||
</div>
|
</div>
|
||||||
{props.paymentStatus && (
|
{props.paymentStatus &&
|
||||||
<h4>
|
(bookingInfo.status === BookingStatus.CANCELLED ||
|
||||||
{!props.paymentStatus.success &&
|
bookingInfo.status === BookingStatus.REJECTED) && (
|
||||||
!props.paymentStatus.refunded &&
|
<h4>
|
||||||
t("booking_with_payment_cancelled")}
|
{!props.paymentStatus.success &&
|
||||||
{props.paymentStatus.success &&
|
!props.paymentStatus.refunded &&
|
||||||
!props.paymentStatus.refunded &&
|
t("booking_with_payment_cancelled")}
|
||||||
t("booking_with_payment_cancelled_already_paid")}
|
{props.paymentStatus.success &&
|
||||||
{props.paymentStatus.refunded && t("booking_with_payment_cancelled_refunded")}
|
!props.paymentStatus.refunded &&
|
||||||
</h4>
|
t("booking_with_payment_cancelled_already_paid")}
|
||||||
)}
|
{props.paymentStatus.refunded && t("booking_with_payment_cancelled_refunded")}
|
||||||
|
</h4>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="border-bookinglightest text-bookingdark dark:border-darkgray-200 mt-8 grid grid-cols-3 border-t pt-8 text-left dark:text-gray-300">
|
<div className="border-bookinglightest text-bookingdark dark:border-darkgray-200 mt-8 grid grid-cols-3 border-t pt-8 text-left dark:text-gray-300">
|
||||||
{(isCancelled || reschedule) && cancellationReason && (
|
{(isCancelled || reschedule) && cancellationReason && (
|
||||||
@@ -497,7 +497,12 @@ export default function Success(props: SuccessProps) {
|
|||||||
<>
|
<>
|
||||||
{bookingInfo?.user && (
|
{bookingInfo?.user && (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<p>{bookingInfo.user.name}</p>
|
<p>
|
||||||
|
<span className="mr-2">{bookingInfo.user.name}</span>
|
||||||
|
<Badge variant="blue" bold>
|
||||||
|
{t("Host")}
|
||||||
|
</Badge>
|
||||||
|
</p>
|
||||||
<p className="text-bookinglight">{bookingInfo.user.email}</p>
|
<p className="text-bookinglight">{bookingInfo.user.email}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -539,63 +544,28 @@ export default function Success(props: SuccessProps) {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{customInputs &&
|
|
||||||
Object.keys(customInputs).map((key) => {
|
{Object.entries(bookingInfo.responses).map(([name, response]) => {
|
||||||
// This breaks if you have two label that are the same.
|
const field = eventType.bookingFields.find((field) => field.name === name);
|
||||||
// TODO: Fix this in another PR
|
// We show location in the "where" section
|
||||||
const customInput = customInputs[key as keyof typeof customInputs];
|
// We show Booker Name, Emails and guests in Who section
|
||||||
const eventTypeCustomFound = eventType.customInputs?.find((ci) => ci.label === key);
|
// We show notes in additional notes section
|
||||||
return (
|
// We show rescheduleReason at the top
|
||||||
<>
|
if (!field) return null;
|
||||||
{eventTypeCustomFound?.type === "RADIO" && (
|
const isSystemField = SystemField.safeParse(field.name);
|
||||||
<>
|
if (isSystemField.success) return null;
|
||||||
<div className="border-bookinglightest dark:border-darkgray-300 col-span-3 mt-8 border-t pt-8 pr-3 font-medium">
|
|
||||||
{eventTypeCustomFound.label}
|
const label = field.label || t(field.defaultLabel || "");
|
||||||
</div>
|
|
||||||
<div className="col-span-3 mt-1 mb-2">
|
return (
|
||||||
{eventTypeCustomFound.options &&
|
<>
|
||||||
eventTypeCustomFound.options.map((option) => {
|
<div className="mt-9 font-medium">{label}</div>
|
||||||
const selected = option.label == customInput;
|
<div className="col-span-2 mb-2 mt-9">
|
||||||
return (
|
<p className="break-words">{response.toString()}</p>
|
||||||
<div
|
</div>
|
||||||
key={option.label}
|
</>
|
||||||
className={classNames(
|
);
|
||||||
"flex space-x-1",
|
})}
|
||||||
!selected && "text-gray-500"
|
|
||||||
)}>
|
|
||||||
<p>{option.label}</p>
|
|
||||||
<span>{option.label === customInput && "✅"}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{eventTypeCustomFound?.type !== "RADIO" && customInput !== "" && (
|
|
||||||
<>
|
|
||||||
<div className="border-bookinglightest dark:border-darkgray-300 col-span-3 mt-8 border-t pt-8 pr-3 font-medium">
|
|
||||||
{key}
|
|
||||||
</div>
|
|
||||||
<div className="col-span-3 mt-2 mb-2">
|
|
||||||
{typeof customInput === "boolean" ? (
|
|
||||||
<p>{customInput ? "true" : "false"}</p>
|
|
||||||
) : (
|
|
||||||
<p>{customInput}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{bookingInfo?.smsReminderNumber && hasSMSAttendeeAction && (
|
|
||||||
<>
|
|
||||||
<div className="mt-9 font-medium">{t("number_sms_notifications")}</div>
|
|
||||||
<div className="col-span-2 mb-2 mt-9">
|
|
||||||
<p>{bookingInfo.smsReminderNumber}</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(!needsConfirmation || !userIsOwner) &&
|
{(!needsConfirmation || !userIsOwner) &&
|
||||||
@@ -826,7 +796,6 @@ export function RecurringBookings({
|
|||||||
t,
|
t,
|
||||||
i18n: { language },
|
i18n: { language },
|
||||||
} = useLocale();
|
} = useLocale();
|
||||||
|
|
||||||
const recurringBookingsSorted = recurringBookings
|
const recurringBookingsSorted = recurringBookings
|
||||||
? recurringBookings.sort((a: ConfigType, b: ConfigType) => (dayjs(a).isAfter(dayjs(b)) ? 1 : -1))
|
? recurringBookings.sort((a: ConfigType, b: ConfigType) => (dayjs(a).isAfter(dayjs(b)) ? 1 : -1))
|
||||||
: null;
|
: null;
|
||||||
@@ -853,7 +822,7 @@ export function RecurringBookings({
|
|||||||
{formatToLocalizedTime(dayjs(dateStr), language, undefined, !is24h)} -{" "}
|
{formatToLocalizedTime(dayjs(dateStr), language, undefined, !is24h)} -{" "}
|
||||||
{formatToLocalizedTime(dayjs(dateStr).add(duration, "m"), language, undefined, !is24h)}{" "}
|
{formatToLocalizedTime(dayjs(dateStr).add(duration, "m"), language, undefined, !is24h)}{" "}
|
||||||
<span className="text-bookinglight">
|
<span className="text-bookinglight">
|
||||||
({formatToLocalizedTimezone(dayjs(dateStr), language)})
|
({formatToLocalizedTimezone(dayjs(dateStr), language, timeZone())})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -873,7 +842,7 @@ export function RecurringBookings({
|
|||||||
{formatToLocalizedTime(date, language, undefined, !is24h)} -{" "}
|
{formatToLocalizedTime(date, language, undefined, !is24h)} -{" "}
|
||||||
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h)}{" "}
|
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h)}{" "}
|
||||||
<span className="text-bookinglight">
|
<span className="text-bookinglight">
|
||||||
({formatToLocalizedTimezone(dayjs(dateStr), language)})
|
({formatToLocalizedTimezone(dayjs(dateStr), language, timeZone())})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -890,7 +859,7 @@ export function RecurringBookings({
|
|||||||
<br />
|
<br />
|
||||||
{formatToLocalizedTime(date, language, undefined, !is24h)} -{" "}
|
{formatToLocalizedTime(date, language, undefined, !is24h)} -{" "}
|
||||||
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h)}{" "}
|
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h)}{" "}
|
||||||
<span className="text-bookinglight">({formatToLocalizedTimezone(date, language)})</span>
|
<span className="text-bookinglight">({formatToLocalizedTimezone(date, language, timeZone())})</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -925,6 +894,8 @@ const getEventTypesFromDB = async (id: number) => {
|
|||||||
locations: true,
|
locations: true,
|
||||||
price: true,
|
price: true,
|
||||||
currency: true,
|
currency: true,
|
||||||
|
bookingFields: true,
|
||||||
|
disableGuests: true,
|
||||||
owner: {
|
owner: {
|
||||||
select: userSelect,
|
select: userSelect,
|
||||||
},
|
},
|
||||||
@@ -949,6 +920,7 @@ const getEventTypesFromDB = async (id: number) => {
|
|||||||
select: {
|
select: {
|
||||||
workflow: {
|
workflow: {
|
||||||
select: {
|
select: {
|
||||||
|
id: true,
|
||||||
steps: true,
|
steps: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -971,6 +943,7 @@ const getEventTypesFromDB = async (id: number) => {
|
|||||||
return {
|
return {
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
...eventType,
|
...eventType,
|
||||||
|
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1015,7 +988,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
if (!parsedQuery.success) return { notFound: true };
|
if (!parsedQuery.success) return { notFound: true };
|
||||||
const { uid, email, eventTypeSlug, cancel } = parsedQuery.data;
|
const { uid, email, eventTypeSlug, cancel } = parsedQuery.data;
|
||||||
|
|
||||||
const bookingInfo = await prisma.booking.findFirst({
|
const bookingInfoRaw = await prisma.booking.findFirst({
|
||||||
where: {
|
where: {
|
||||||
uid,
|
uid,
|
||||||
},
|
},
|
||||||
@@ -1033,6 +1006,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
status: true,
|
status: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
cancellationReason: true,
|
cancellationReason: true,
|
||||||
|
responses: true,
|
||||||
rejectionReason: true,
|
rejectionReason: true,
|
||||||
user: {
|
user: {
|
||||||
select: {
|
select: {
|
||||||
@@ -1057,27 +1031,28 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (!bookingInfoRaw) {
|
||||||
if (!bookingInfo) {
|
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
const eventTypeRaw = !bookingInfoRaw.eventTypeId
|
||||||
// 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;
|
|
||||||
|
|
||||||
const eventTypeRaw = !bookingInfo.eventTypeId
|
|
||||||
? getDefaultEvent(eventTypeSlug || "")
|
? getDefaultEvent(eventTypeSlug || "")
|
||||||
: await getEventTypesFromDB(bookingInfo.eventTypeId);
|
: await getEventTypesFromDB(bookingInfoRaw.eventTypeId);
|
||||||
if (!eventTypeRaw) {
|
if (!eventTypeRaw) {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bookingInfo = getBookingWithResponses(bookingInfoRaw, eventTypeRaw);
|
||||||
|
|
||||||
|
// @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.users = !!eventTypeRaw.hosts?.length
|
||||||
? eventTypeRaw.hosts.map((host) => host.user)
|
? eventTypeRaw.hosts.map((host) => host.user)
|
||||||
: eventTypeRaw.users;
|
: eventTypeRaw.users;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { GetServerSidePropsContext } from "next";
|
|||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { bookEventTypeSelect } from "@calcom/prisma/selects";
|
import { bookEventTypeSelect } from "@calcom/prisma/selects";
|
||||||
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
@@ -71,6 +71,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
...eventTypeRaw,
|
...eventTypeRaw,
|
||||||
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}),
|
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}),
|
||||||
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
||||||
|
bookingFields: eventTypeBookingFields.parse(eventTypeRaw.bookingFields || []),
|
||||||
};
|
};
|
||||||
|
|
||||||
const eventTypeObject = [eventType].map((e) => {
|
const eventTypeObject = [eventType].map((e) => {
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import type { PeriodType } from "@prisma/client";
|
import type { PeriodType } from "@prisma/client";
|
||||||
import { SchedulingType } from "@prisma/client";
|
import { SchedulingType } from "@prisma/client";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { validateCustomEventName } from "@calcom/core/event";
|
||||||
import type { EventLocationType } from "@calcom/core/location";
|
import type { EventLocationType } from "@calcom/core/location";
|
||||||
import { validateBookingLimitOrder } from "@calcom/lib";
|
import { validateBookingLimitOrder } from "@calcom/lib";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
@@ -16,6 +17,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||||
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -86,6 +88,7 @@ export type FormValues = {
|
|||||||
bookingLimits?: BookingLimit;
|
bookingLimits?: BookingLimit;
|
||||||
hosts: { userId: number }[];
|
hosts: { userId: number }[];
|
||||||
hostsFixed: { userId: number }[];
|
hostsFixed: { userId: number }[];
|
||||||
|
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomInputParsed = typeof customInputSchema._output;
|
export type CustomInputParsed = typeof customInputSchema._output;
|
||||||
@@ -120,7 +123,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
extendsFeature: "EventType",
|
extendsFeature: "EventType",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { eventType, locationOptions, team, teamMembers, currentUserMembership } = props;
|
const { eventType, locationOptions, team, teamMembers, currentUserMembership, destinationCalendar } = props;
|
||||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
|
|
||||||
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
||||||
@@ -178,48 +181,65 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
delete metadata.config?.useHostSchedulesForTeamEvent;
|
delete metadata.config?.useHostSchedulesForTeamEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formMethods = useForm<FormValues>({
|
const defaultValues = {
|
||||||
defaultValues: {
|
title: eventType.title,
|
||||||
title: eventType.title,
|
locations: eventType.locations || [],
|
||||||
locations: eventType.locations || [],
|
recurringEvent: eventType.recurringEvent || null,
|
||||||
recurringEvent: eventType.recurringEvent || null,
|
description: eventType.description ?? undefined,
|
||||||
description: eventType.description ?? undefined,
|
schedule: eventType.schedule || undefined,
|
||||||
schedule: eventType.schedule || undefined,
|
bookingLimits: eventType.bookingLimits || undefined,
|
||||||
bookingLimits: eventType.bookingLimits || undefined,
|
length: eventType.length,
|
||||||
length: eventType.length,
|
hidden: eventType.hidden,
|
||||||
hidden: eventType.hidden,
|
periodDates: {
|
||||||
periodDates: {
|
startDate: periodDates.startDate,
|
||||||
startDate: periodDates.startDate,
|
endDate: periodDates.endDate,
|
||||||
endDate: periodDates.endDate,
|
|
||||||
},
|
|
||||||
periodType: eventType.periodType,
|
|
||||||
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
|
|
||||||
schedulingType: eventType.schedulingType,
|
|
||||||
minimumBookingNotice: eventType.minimumBookingNotice,
|
|
||||||
metadata,
|
|
||||||
hosts: !!eventType.hosts?.length
|
|
||||||
? eventType.hosts.filter((host) => !host.isFixed)
|
|
||||||
: eventType.users
|
|
||||||
.filter(() => eventType.schedulingType === SchedulingType.ROUND_ROBIN)
|
|
||||||
.map((user) => ({ userId: user.id })),
|
|
||||||
hostsFixed: !!eventType.hosts?.length
|
|
||||||
? eventType.hosts.filter((host) => host.isFixed)
|
|
||||||
: eventType.users
|
|
||||||
.filter(() => eventType.schedulingType === SchedulingType.COLLECTIVE)
|
|
||||||
.map((user) => ({ userId: user.id })),
|
|
||||||
},
|
},
|
||||||
|
bookingFields: eventType.bookingFields,
|
||||||
|
periodType: eventType.periodType,
|
||||||
|
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
|
||||||
|
schedulingType: eventType.schedulingType,
|
||||||
|
minimumBookingNotice: eventType.minimumBookingNotice,
|
||||||
|
metadata,
|
||||||
|
hosts: !!eventType.hosts?.length
|
||||||
|
? eventType.hosts.filter((host) => !host.isFixed)
|
||||||
|
: eventType.users
|
||||||
|
.filter(() => eventType.schedulingType === SchedulingType.ROUND_ROBIN)
|
||||||
|
.map((user) => ({ userId: user.id })),
|
||||||
|
hostsFixed: !!eventType.hosts?.length
|
||||||
|
? eventType.hosts.filter((host) => host.isFixed)
|
||||||
|
: eventType.users
|
||||||
|
.filter(() => eventType.schedulingType === SchedulingType.COLLECTIVE)
|
||||||
|
.map((user) => ({ userId: user.id })),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const formMethods = useForm<FormValues>({
|
||||||
|
defaultValues,
|
||||||
resolver: zodResolver(
|
resolver: zodResolver(
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
// Length if string, is converted to a number or it can be a number
|
// Length if string, is converted to a number or it can be a number
|
||||||
// Make it optional because it's not submitted from all tabs of the page
|
// Make it optional because it's not submitted from all tabs of the page
|
||||||
|
eventName: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => validateCustomEventName(val, t("invalid_event_name_variables")) === true, {
|
||||||
|
message: t("invalid_event_name_variables"),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
|
length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
|
||||||
|
bookingFields: eventTypeBookingFields,
|
||||||
})
|
})
|
||||||
// TODO: Add schema for other fields later.
|
// TODO: Add schema for other fields later.
|
||||||
.passthrough()
|
.passthrough()
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!formMethods.formState.isDirty) {
|
||||||
|
//TODO: What's the best way to sync the form with backend
|
||||||
|
formMethods.setValue("bookingFields", defaultValues.bookingFields);
|
||||||
|
}
|
||||||
|
}, [defaultValues]);
|
||||||
|
|
||||||
const appsMetadata = formMethods.getValues("metadata")?.apps;
|
const appsMetadata = formMethods.getValues("metadata")?.apps;
|
||||||
const numberOfInstalledApps = eventTypeApps?.filter((app) => app.isInstalled).length || 0;
|
const numberOfInstalledApps = eventTypeApps?.filter((app) => app.isInstalled).length || 0;
|
||||||
let numberOfActiveApps = 0;
|
let numberOfActiveApps = 0;
|
||||||
@@ -241,6 +261,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
locationOptions={locationOptions}
|
locationOptions={locationOptions}
|
||||||
team={team}
|
team={team}
|
||||||
teamMembers={teamMembers}
|
teamMembers={teamMembers}
|
||||||
|
destinationCalendar={destinationCalendar}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
availability: <AvailabilityTab isTeamEvent={!!team} />,
|
availability: <AvailabilityTab isTeamEvent={!!team} />,
|
||||||
@@ -342,13 +363,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => {
|
const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
const { data, isLoading } = trpc.viewer.eventTypes.get.useQuery(
|
const { data, isLoading } = trpc.viewer.eventTypes.get.useQuery({ id: props.type });
|
||||||
{ id: props.type },
|
|
||||||
{
|
|
||||||
initialData: props.initialData,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoading || !data) return null;
|
if (isLoading || !data) return null;
|
||||||
return <EventTypePage {...data} />;
|
return <EventTypePage {...data} />;
|
||||||
};
|
};
|
||||||
@@ -391,9 +406,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return {
|
throw err;
|
||||||
notFound: true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useEffect, useState, memo } from "react";
|
import { useEffect, useState, memo } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||||
CreateEventTypeButton,
|
import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
|
||||||
EventTypeDescriptionLazy as EventTypeDescription,
|
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
|
||||||
} from "@calcom/features/eventtypes/components";
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -36,6 +34,7 @@ import {
|
|||||||
showToast,
|
showToast,
|
||||||
Switch,
|
Switch,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
CreateButton,
|
||||||
HorizontalTabs,
|
HorizontalTabs,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import {
|
import {
|
||||||
@@ -60,8 +59,6 @@ import { HttpError } from "@lib/core/http/error";
|
|||||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||||
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
|
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
|
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
|
||||||
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
|
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
|
||||||
|
|
||||||
@@ -73,6 +70,7 @@ interface EventTypeListHeadingProps {
|
|||||||
|
|
||||||
type EventTypeGroup = EventTypeGroups[number];
|
type EventTypeGroup = EventTypeGroups[number];
|
||||||
type EventType = EventTypeGroup["eventTypes"][number];
|
type EventType = EventTypeGroup["eventTypes"][number];
|
||||||
|
|
||||||
interface EventTypeListProps {
|
interface EventTypeListProps {
|
||||||
group: EventTypeGroup;
|
group: EventTypeGroup;
|
||||||
groupIndex: number;
|
groupIndex: number;
|
||||||
@@ -121,7 +119,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
|||||||
<Link
|
<Link
|
||||||
href={`/event-types/${type.id}?tabName=setup`}
|
href={`/event-types/${type.id}?tabName=setup`}
|
||||||
className="flex-1 overflow-hidden pr-4 text-sm"
|
className="flex-1 overflow-hidden pr-4 text-sm"
|
||||||
title={`${type.title} ${type.description ? `– ${type.description}` : ""}`}>
|
title={type.title}>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
className="font-semibold text-gray-700 ltr:mr-1 rtl:ml-1"
|
className="font-semibold text-gray-700 ltr:mr-1 rtl:ml-1"
|
||||||
@@ -134,18 +132,17 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
|||||||
data-testid={"event-type-slug-" + type.id}>
|
data-testid={"event-type-slug-" + type.id}>
|
||||||
{`/${group.profile.slug}/${type.slug}`}
|
{`/${group.profile.slug}/${type.slug}`}
|
||||||
</small>
|
</small>
|
||||||
) : (
|
) : null}
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
{readOnly && (
|
{readOnly && (
|
||||||
<span className="inline items-center rounded-sm bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800 ltr:ml-2 ltr:mr-2 rtl:ml-2">
|
<Badge variant="gray" className="ml-2">
|
||||||
{t("readonly")}
|
{t("readonly")}
|
||||||
</span>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<EventTypeDescription
|
<EventTypeDescription
|
||||||
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
|
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
|
||||||
eventType={type}
|
eventType={type}
|
||||||
|
shortenDescription
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -244,7 +241,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
const openDuplicateModal = (eventType: EventType, group: EventTypeGroup) => {
|
const openDuplicateModal = (eventType: EventType, group: EventTypeGroup) => {
|
||||||
const query = {
|
const query = {
|
||||||
...router.query,
|
...router.query,
|
||||||
dialog: "duplicate-event-type",
|
dialog: "duplicate",
|
||||||
title: eventType.title,
|
title: eventType.title,
|
||||||
description: eventType.description,
|
description: eventType.description,
|
||||||
slug: eventType.slug,
|
slug: eventType.slug,
|
||||||
@@ -631,17 +628,37 @@ const CreateFirstEventTypeView = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CTA = () => {
|
const CTA = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
const query = trpc.viewer.eventTypes.getByViewer.useQuery();
|
const query = trpc.viewer.eventTypes.getByViewer.useQuery();
|
||||||
|
|
||||||
if (!query.data) return null;
|
if (!query.data) return null;
|
||||||
|
|
||||||
return <CreateEventTypeButton canAddEvents={true} options={query.data.profiles} />;
|
const profileOptions = query.data.profiles
|
||||||
|
.filter((profile) => !profile.readOnly)
|
||||||
|
.map((profile) => {
|
||||||
|
return {
|
||||||
|
teamId: profile.teamId,
|
||||||
|
label: profile.name || profile.slug,
|
||||||
|
image: profile.image,
|
||||||
|
slug: profile.slug,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CreateButton
|
||||||
|
subtitle={t("create_event_on").toUpperCase()}
|
||||||
|
options={profileOptions}
|
||||||
|
createDialog={CreateEventTypeDialog}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer);
|
const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer);
|
||||||
|
|
||||||
const EventTypesPage = () => {
|
const EventTypesPage = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||||
|
|
||||||
@@ -687,6 +704,7 @@ const EventTypesPage = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<EmbedDialog />
|
<EmbedDialog />
|
||||||
|
{router.query.dialog === "duplicate" && <DuplicateDialog />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -695,14 +713,4 @@ const EventTypesPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EventTypesPage;
|
export default EventTypesPage;
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
|
|
||||||
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Meta } from "@calcom/ui";
|
import { Meta } from "@calcom/ui";
|
||||||
|
|
||||||
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
function AdminAppsView() {
|
function AdminAppsView() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
@@ -20,14 +16,4 @@ function AdminAppsView() {
|
|||||||
|
|
||||||
AdminAppsView.getLayout = getLayout;
|
AdminAppsView.getLayout = getLayout;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdminAppsView;
|
export default AdminAppsView;
|
||||||
@@ -1,2 +1 @@
|
|||||||
export { getServerSideProps } from "./[category]";
|
|
||||||
export { default } from "./[category]";
|
export { default } from "./[category]";
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
|
|
||||||
@@ -7,8 +6,6 @@ import { Button, Meta, TextField } from "@calcom/ui";
|
|||||||
|
|
||||||
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
function AdminView() {
|
function AdminView() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const usernameRef = useRef<HTMLInputElement>(null);
|
const usernameRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -41,14 +38,4 @@ function AdminView() {
|
|||||||
|
|
||||||
AdminView.getLayout = getLayout;
|
AdminView.getLayout = getLayout;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdminView;
|
export default AdminView;
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
|
|
||||||
import { Meta } from "@calcom/ui";
|
import { Meta } from "@calcom/ui";
|
||||||
|
|
||||||
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
function AdminAppsView() {
|
function AdminAppsView() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -17,14 +13,4 @@ function AdminAppsView() {
|
|||||||
|
|
||||||
AdminAppsView.getLayout = getLayout;
|
AdminAppsView.getLayout = getLayout;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdminAppsView;
|
export default AdminAppsView;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { HelpScout, useChat } from "react-live-chat-loader";
|
import { HelpScout, useChat } from "react-live-chat-loader";
|
||||||
@@ -10,8 +9,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { Button, Meta } from "@calcom/ui";
|
import { Button, Meta } from "@calcom/ui";
|
||||||
import { FiExternalLink } from "@calcom/ui/components/icon";
|
import { FiExternalLink } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
interface CtaRowProps {
|
interface CtaRowProps {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -72,14 +69,4 @@ const BillingView = () => {
|
|||||||
|
|
||||||
BillingView.getLayout = getLayout;
|
BillingView.getLayout = getLayout;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BillingView;
|
export default BillingView;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import type { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem";
|
import type { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem";
|
||||||
@@ -19,8 +18,6 @@ import {
|
|||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiLink, FiPlus } from "@calcom/ui/components/icon";
|
import { FiLink, FiPlus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
const ApiKeysView = () => {
|
const ApiKeysView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
@@ -96,14 +93,4 @@ const ApiKeysView = () => {
|
|||||||
|
|
||||||
ApiKeysView.getLayout = getLayout;
|
ApiKeysView.getLayout = getLayout;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ApiKeysView;
|
export default ApiKeysView;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
import ThemeLabel from "@calcom/features/settings/ThemeLabel";
|
import ThemeLabel from "@calcom/features/settings/ThemeLabel";
|
||||||
@@ -20,8 +19,6 @@ import {
|
|||||||
UpgradeTeamsBadge,
|
UpgradeTeamsBadge,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
@@ -215,14 +212,4 @@ const AppearanceView = () => {
|
|||||||
|
|
||||||
AppearanceView.getLayout = getLayout;
|
AppearanceView.getLayout = getLayout;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppearanceView;
|
export default AppearanceView;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -30,8 +29,6 @@ import { QueryCell } from "@lib/QueryCell";
|
|||||||
|
|
||||||
import { CalendarSwitch } from "@components/settings/CalendarSwitch";
|
import { CalendarSwitch } from "@components/settings/CalendarSwitch";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
const SkeletonLoader = () => {
|
const SkeletonLoader = () => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
@@ -228,14 +225,4 @@ const CalendarsView = () => {
|
|||||||
|
|
||||||
CalendarsView.getLayout = getLayout;
|
CalendarsView.getLayout = getLayout;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarsView;
|
export default CalendarsView;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
|
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
|
||||||
import { AppSetDefaultLinkDailog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
|
import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
|
||||||
|
import { BulkEditDefaultConferencingModal } from "@calcom/features/eventtypes/components/BulkEditDefaultConferencingModal";
|
||||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -62,17 +63,20 @@ const ConferencingLayout = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onSuccessCallback = useCallback(() => {
|
||||||
|
setBulkUpdateModal(true);
|
||||||
|
showToast("Default app updated successfully", "success");
|
||||||
|
}, []);
|
||||||
|
|
||||||
const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({
|
const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: onSuccessCallback,
|
||||||
showToast("Default app updated successfully", "success");
|
|
||||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showToast(`Error: ${error.message}`, "error");
|
showToast(`Error: ${error.message}`, "error");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [deleteAppModal, setDeleteAppModal] = useState(false);
|
const [deleteAppModal, setDeleteAppModal] = useState(false);
|
||||||
|
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
|
||||||
const [locationType, setLocationType] = useState<(EventLocationType & { slug: string }) | undefined>(
|
const [locationType, setLocationType] = useState<(EventLocationType & { slug: string }) | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
@@ -167,7 +171,14 @@ const ConferencingLayout = () => {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{locationType && (
|
{locationType && (
|
||||||
<AppSetDefaultLinkDailog locationType={locationType} setLocationType={setLocationType} />
|
<AppSetDefaultLinkDialog
|
||||||
|
locationType={locationType}
|
||||||
|
setLocationType={setLocationType}
|
||||||
|
onSuccess={onSuccessCallback}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{bulkUpdateModal && (
|
||||||
|
<BulkEditDefaultConferencingModal open={bulkUpdateModal} setOpen={setBulkUpdateModal} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
@@ -23,8 +22,6 @@ import {
|
|||||||
|
|
||||||
import { withQuery } from "@lib/QueryCell";
|
import { withQuery } from "@lib/QueryCell";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
@@ -223,14 +220,4 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||||||
|
|
||||||
GeneralQueryView.getLayout = getLayout;
|
GeneralQueryView.getLayout = getLayout;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GeneralQueryView;
|
export default GeneralQueryView;
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { IdentityProvider } from "@prisma/client";
|
import { IdentityProvider } from "@prisma/client";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import MarkdownIt from "markdown-it";
|
|
||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
import type { BaseSyntheticEvent } from "react";
|
import type { BaseSyntheticEvent } from "react";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
@@ -11,6 +9,7 @@ import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
|||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
import turndown from "@calcom/lib/turndownService";
|
import turndown from "@calcom/lib/turndownService";
|
||||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -42,10 +41,6 @@ import { FiAlertTriangle, FiTrash2 } from "@calcom/ui/components/icon";
|
|||||||
import TwoFactor from "@components/auth/TwoFactor";
|
import TwoFactor from "@components/auth/TwoFactor";
|
||||||
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
const md = new MarkdownIt("default", { html: true, breaks: true });
|
|
||||||
|
|
||||||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
@@ -374,6 +369,7 @@ const ProfileForm = ({
|
|||||||
formMethods.setValue("bio", turndown(value), { shouldDirty: true });
|
formMethods.setValue("bio", turndown(value), { shouldDirty: true });
|
||||||
}}
|
}}
|
||||||
excludedToolbarItems={["blockType"]}
|
excludedToolbarItems={["blockType"]}
|
||||||
|
disableLists
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button disabled={isDisabled} color="primary" className="mt-8" type="submit">
|
<Button disabled={isDisabled} color="primary" className="mt-8" type="submit">
|
||||||
@@ -385,14 +381,4 @@ const ProfileForm = ({
|
|||||||
|
|
||||||
ProfileView.getLayout = getLayout;
|
ProfileView.getLayout = getLayout;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProfileView;
|
export default ProfileView;
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { IdentityProvider } from "@prisma/client";
|
import { IdentityProvider } from "@prisma/client";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { signOut, useSession } from "next-auth/react";
|
import { signOut, useSession } from "next-auth/react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
@@ -10,8 +9,6 @@ import { userMetadata } from "@calcom/prisma/zod-utils";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Alert, Button, Form, Meta, PasswordField, Select, SettingsToggle, showToast } from "@calcom/ui";
|
import { Alert, Button, Form, Meta, PasswordField, Select, SettingsToggle, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
type ChangePasswordSessionFormValues = {
|
type ChangePasswordSessionFormValues = {
|
||||||
oldPassword: string;
|
oldPassword: string;
|
||||||
newPassword: string;
|
newPassword: string;
|
||||||
@@ -209,14 +206,4 @@ const PasswordView = () => {
|
|||||||
|
|
||||||
PasswordView.getLayout = getLayout;
|
PasswordView.getLayout = getLayout;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PasswordView;
|
export default PasswordView;
|
||||||
@@ -1,15 +1 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/sso/page/user-sso-view";
|
export { default } from "@calcom/features/ee/sso/page/user-sso-view";
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
@@ -9,8 +8,6 @@ import { Badge, Meta, Switch, SkeletonButton, SkeletonContainer, SkeletonText }
|
|||||||
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
|
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
|
||||||
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
|
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
const SkeletonLoader = () => {
|
const SkeletonLoader = () => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
@@ -85,14 +82,4 @@ const TwoFactorAuthView = () => {
|
|||||||
|
|
||||||
TwoFactorAuthView.getLayout = getLayout;
|
TwoFactorAuthView.getLayout = getLayout;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TwoFactorAuthView;
|
export default TwoFactorAuthView;
|
||||||
@@ -1,15 +1 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/teams/pages/team-appearance-view";
|
export { default } from "@calcom/features/ee/teams/pages/team-appearance-view";
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,15 +1 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/teams/pages/team-billing-view";
|
export { default } from "@calcom/features/ee/teams/pages/team-billing-view";
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,15 +1 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/teams/pages/team-members-view";
|
export { default } from "@calcom/features/ee/teams/pages/team-members-view";
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,15 +1 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/teams/pages/team-profile-view";
|
export { default } from "@calcom/features/ee/teams/pages/team-profile-view";
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,15 +1 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/sso/page/teams-sso-view";
|
export { default } from "@calcom/features/ee/sso/page/teams-sso-view";
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
|
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
|
||||||
@@ -6,8 +5,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
|
|
||||||
import { getLayout } from "@components/layouts/WizardLayout";
|
import { getLayout } from "@components/layouts/WizardLayout";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
const CreateNewTeamPage = () => {
|
const CreateNewTeamPage = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
@@ -23,14 +20,4 @@ const CreateNewTeamPage = () => {
|
|||||||
|
|
||||||
CreateNewTeamPage.getLayout = getLayout;
|
CreateNewTeamPage.getLayout = getLayout;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CreateNewTeamPage;
|
export default CreateNewTeamPage;
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import MarkdownIt from "markdown-it";
|
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -11,6 +10,7 @@ import { CAL_URL } from "@calcom/lib/constants";
|
|||||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { Avatar, Button, HeadSeo, AvatarGroup } from "@calcom/ui";
|
import { Avatar, Button, HeadSeo, AvatarGroup } from "@calcom/ui";
|
||||||
@@ -23,8 +23,6 @@ import Team from "@components/team/screens/Team";
|
|||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
|
||||||
|
|
||||||
export type TeamPageProps = inferSSRProps<typeof getServerSideProps>;
|
export type TeamPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
function TeamPage({ team }: TeamPageProps) {
|
function TeamPage({ team }: TeamPageProps) {
|
||||||
useTheme(team.theme);
|
useTheme(team.theme);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
|||||||
|
|
||||||
import type { LocationObject } from "@calcom/core/location";
|
import type { LocationObject } from "@calcom/core/location";
|
||||||
import { privacyFilteredLocations } from "@calcom/core/location";
|
import { privacyFilteredLocations } from "@calcom/core/location";
|
||||||
|
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import { getWorkingHours } from "@calcom/lib/availability";
|
import { getWorkingHours } from "@calcom/lib/availability";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
@@ -78,6 +79,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
availability: true,
|
availability: true,
|
||||||
description: true,
|
description: true,
|
||||||
length: true,
|
length: true,
|
||||||
|
disableGuests: true,
|
||||||
schedulingType: true,
|
schedulingType: true,
|
||||||
periodType: true,
|
periodType: true,
|
||||||
periodStartDate: true,
|
periodStartDate: true,
|
||||||
@@ -96,12 +98,24 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
slotInterval: true,
|
slotInterval: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
seatsPerTimeSlot: true,
|
seatsPerTimeSlot: true,
|
||||||
|
bookingFields: true,
|
||||||
|
customInputs: true,
|
||||||
schedule: {
|
schedule: {
|
||||||
select: {
|
select: {
|
||||||
timeZone: true,
|
timeZone: true,
|
||||||
availability: true,
|
availability: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
workflows: {
|
||||||
|
select: {
|
||||||
|
workflow: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
steps: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
team: {
|
team: {
|
||||||
select: {
|
select: {
|
||||||
members: {
|
members: {
|
||||||
@@ -163,7 +177,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
|
|
||||||
let booking: GetBookingType | null = null;
|
let booking: GetBookingType | null = null;
|
||||||
if (rescheduleUid) {
|
if (rescheduleUid) {
|
||||||
booking = await getBooking(prisma, rescheduleUid);
|
booking = await getBooking(prisma, rescheduleUid, getBookingFieldsWithSystemFields(eventTypeObject));
|
||||||
}
|
}
|
||||||
|
|
||||||
const weekStart = eventType.team?.members?.[0]?.user?.weekStart;
|
const weekStart = eventType.team?.members?.[0]?.user?.weekStart;
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import type { GetServerSidePropsContext } from "next";
|
|||||||
|
|
||||||
import type { LocationObject } from "@calcom/app-store/locations";
|
import type { LocationObject } from "@calcom/app-store/locations";
|
||||||
import { privacyFilteredLocations } from "@calcom/app-store/locations";
|
import { privacyFilteredLocations } from "@calcom/app-store/locations";
|
||||||
|
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
||||||
import type { GetBookingType } from "@lib/getBooking";
|
import type { GetBookingType } from "@lib/getBooking";
|
||||||
@@ -13,6 +14,8 @@ import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
|||||||
|
|
||||||
import BookingPage from "@components/booking/pages/BookingPage";
|
import BookingPage from "@components/booking/pages/BookingPage";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export type TeamBookingPageProps = inferSSRProps<typeof getServerSideProps>;
|
export type TeamBookingPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
|
|
||||||
export default function TeamBookingPage(props: TeamBookingPageProps) {
|
export default function TeamBookingPage(props: TeamBookingPageProps) {
|
||||||
@@ -22,6 +25,7 @@ export default function TeamBookingPage(props: TeamBookingPageProps) {
|
|||||||
TeamBookingPage.isThemeSupported = true;
|
TeamBookingPage.isThemeSupported = true;
|
||||||
|
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
const eventTypeId = parseInt(asStringOrThrow(context.query.type));
|
const eventTypeId = parseInt(asStringOrThrow(context.query.type));
|
||||||
const recurringEventCountQuery = asStringOrNull(context.query.count);
|
const recurringEventCountQuery = asStringOrNull(context.query.count);
|
||||||
if (typeof eventTypeId !== "number" || eventTypeId % 1 !== 0) {
|
if (typeof eventTypeId !== "number" || eventTypeId % 1 !== 0) {
|
||||||
@@ -55,6 +59,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
metadata: true,
|
metadata: true,
|
||||||
seatsPerTimeSlot: true,
|
seatsPerTimeSlot: true,
|
||||||
schedulingType: true,
|
schedulingType: true,
|
||||||
|
bookingFields: true,
|
||||||
workflows: {
|
workflows: {
|
||||||
include: {
|
include: {
|
||||||
workflow: {
|
workflow: {
|
||||||
@@ -92,12 +97,13 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
//TODO: Use zodSchema to verify it instead of using Type Assertion
|
//TODO: Use zodSchema to verify it instead of using Type Assertion
|
||||||
locations: privacyFilteredLocations((eventTypeRaw.locations || []) as LocationObject[]),
|
locations: privacyFilteredLocations((eventTypeRaw.locations || []) as LocationObject[]),
|
||||||
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
||||||
|
bookingFields: eventTypeBookingFields.parse(eventTypeRaw.bookingFields || []),
|
||||||
};
|
};
|
||||||
|
|
||||||
const eventTypeObject = [eventType].map((e) => {
|
const eventTypeObject = [eventType].map((e) => {
|
||||||
return {
|
return {
|
||||||
...e,
|
...e,
|
||||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
metadata: EventTypeMetaDataSchema.parse(e.metadata || {}),
|
||||||
|
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
||||||
periodStartDate: e.periodStartDate?.toString() ?? null,
|
periodStartDate: e.periodStartDate?.toString() ?? null,
|
||||||
periodEndDate: e.periodEndDate?.toString() ?? null,
|
periodEndDate: e.periodEndDate?.toString() ?? null,
|
||||||
customInputs: customInputSchema.array().parse(e.customInputs || []),
|
customInputs: customInputSchema.array().parse(e.customInputs || []),
|
||||||
@@ -114,7 +120,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
|
|
||||||
let booking: GetBookingType | null = null;
|
let booking: GetBookingType | null = null;
|
||||||
if (context.query.rescheduleUid) {
|
if (context.query.rescheduleUid) {
|
||||||
booking = await getBooking(prisma, context.query.rescheduleUid as string);
|
booking = await getBooking(prisma, context.query.rescheduleUid as string, eventTypeObject.bookingFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checking if number of recurring event ocurrances is valid against event type configuration
|
// Checking if number of recurring event ocurrances is valid against event type configuration
|
||||||
@@ -128,6 +134,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
profile: {
|
profile: {
|
||||||
...eventTypeObject.team,
|
...eventTypeObject.team,
|
||||||
// FIXME: This slug is used as username on success page which is wrong. This is correctly set as username for user booking.
|
// FIXME: This slug is used as username on success page which is wrong. This is correctly set as username for user booking.
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
|
|
||||||
import { TeamsListing } from "@calcom/features/ee/teams/components";
|
import { TeamsListing } from "@calcom/features/ee/teams/components";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
@@ -7,8 +5,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { Button } from "@calcom/ui";
|
import { Button } from "@calcom/ui";
|
||||||
import { FiPlus } from "@calcom/ui/components/icon";
|
import { FiPlus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
function Teams() {
|
function Teams() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
@@ -31,14 +27,4 @@ function Teams() {
|
|||||||
|
|
||||||
Teams.requiresLicense = false;
|
Teams.requiresLicense = false;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Teams;
|
export default Teams;
|
||||||
@@ -1,15 +1 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/workflows/pages/index";
|
export { default } from "@calcom/features/ee/workflows/pages/index";
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -4,6 +4,8 @@ import { test } from "./lib/fixtures";
|
|||||||
|
|
||||||
test.describe.configure({ mode: "parallel" });
|
test.describe.configure({ mode: "parallel" });
|
||||||
|
|
||||||
|
test.afterEach(({ users }) => users.deleteAll());
|
||||||
|
|
||||||
test.describe("App Store - Authed", () => {
|
test.describe("App Store - Authed", () => {
|
||||||
test("Browse apple-calendar and try to install", async ({ page, users }) => {
|
test("Browse apple-calendar and try to install", async ({ page, users }) => {
|
||||||
const pro = await users.create();
|
const pro = await users.create();
|
||||||
@@ -12,7 +14,6 @@ test.describe("App Store - Authed", () => {
|
|||||||
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
|
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
|
||||||
await page.click('[data-testid="install-app-button"]');
|
await page.click('[data-testid="install-app-button"]');
|
||||||
await expect(page.locator(`text=Connect to Apple Server`)).toBeVisible();
|
await expect(page.locator(`text=Connect to Apple Server`)).toBeVisible();
|
||||||
await pro.delete();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Installed Apps - Navigation", async ({ page, users }) => {
|
test("Installed Apps - Navigation", async ({ page, users }) => {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { expect } from "@playwright/test";
|
|||||||
|
|
||||||
import { test } from "../lib/fixtures";
|
import { test } from "../lib/fixtures";
|
||||||
|
|
||||||
|
test.afterEach(({ users }) => users.deleteAll());
|
||||||
|
|
||||||
test("Can delete user account", async ({ page, users }) => {
|
test("Can delete user account", async ({ page, users }) => {
|
||||||
const user = await users.create({
|
const user = await users.create({
|
||||||
username: "delete-me",
|
username: "delete-me",
|
||||||
@@ -10,7 +12,7 @@ test("Can delete user account", async ({ page, users }) => {
|
|||||||
|
|
||||||
await page.waitForSelector("[data-testid=dashboard-shell]");
|
await page.waitForSelector("[data-testid=dashboard-shell]");
|
||||||
|
|
||||||
await page.goto(`/settings/profile`);
|
await page.goto(`/settings/my-account/profile`);
|
||||||
await page.click("[data-testid=delete-account]");
|
await page.click("[data-testid=delete-account]");
|
||||||
if (!user.username) throw Error(`Test user doesn't have a username`);
|
if (!user.username) throw Error(`Test user doesn't have a username`);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { expect } from "@playwright/test";
|
|||||||
|
|
||||||
import { test } from "../lib/fixtures";
|
import { test } from "../lib/fixtures";
|
||||||
|
|
||||||
|
test.afterEach(({ users }) => users.deleteAll());
|
||||||
|
|
||||||
test("Can reset forgotten password", async ({ page, users }) => {
|
test("Can reset forgotten password", async ({ page, users }) => {
|
||||||
test.skip(process.env.NEXT_PUBLIC_IS_E2E !== "1", "It shouldn't if we can't skip email");
|
test.skip(process.env.NEXT_PUBLIC_IS_E2E !== "1", "It shouldn't if we can't skip email");
|
||||||
const user = await users.create();
|
const user = await users.create();
|
||||||
@@ -47,6 +49,4 @@ test("Can reset forgotten password", async ({ page, users }) => {
|
|||||||
await page.waitForSelector("[data-testid=dashboard-shell]");
|
await page.waitForSelector("[data-testid=dashboard-shell]");
|
||||||
|
|
||||||
await expect(page.locator("[data-testid=dashboard-shell]")).toBeVisible();
|
await expect(page.locator("[data-testid=dashboard-shell]")).toBeVisible();
|
||||||
|
|
||||||
await users.deleteAll();
|
|
||||||
});
|
});
|
||||||
@@ -10,15 +10,13 @@ import {
|
|||||||
} from "./lib/testUtils";
|
} from "./lib/testUtils";
|
||||||
|
|
||||||
test.describe.configure({ mode: "parallel" });
|
test.describe.configure({ mode: "parallel" });
|
||||||
|
test.afterEach(async ({ users }) => users.deleteAll());
|
||||||
|
|
||||||
test.describe("free user", () => {
|
test.describe("free user", () => {
|
||||||
test.beforeEach(async ({ page, users }) => {
|
test.beforeEach(async ({ page, users }) => {
|
||||||
const free = await users.create();
|
const free = await users.create();
|
||||||
await page.goto(`/${free.username}`);
|
await page.goto(`/${free.username}`);
|
||||||
});
|
});
|
||||||
test.afterEach(async ({ users }) => {
|
|
||||||
await users.deleteAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("cannot book same slot multiple times", async ({ page }) => {
|
test("cannot book same slot multiple times", async ({ page }) => {
|
||||||
// Click first event type
|
// Click first event type
|
||||||
@@ -58,9 +56,6 @@ test.describe("pro user", () => {
|
|||||||
const pro = await users.create();
|
const pro = await users.create();
|
||||||
await page.goto(`/${pro.username}`);
|
await page.goto(`/${pro.username}`);
|
||||||
});
|
});
|
||||||
test.afterEach(async ({ users }) => {
|
|
||||||
await users.deleteAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("pro user's page has at least 2 visible events", async ({ page }) => {
|
test("pro user's page has at least 2 visible events", async ({ page }) => {
|
||||||
// await page.pause();
|
// await page.pause();
|
||||||
|
|||||||
@@ -24,6 +24,6 @@ test.describe("Change Password Test", () => {
|
|||||||
|
|
||||||
const toast = await page.waitForSelector("div[class*='data-testid-toast-success']");
|
const toast = await page.waitForSelector("div[class*='data-testid-toast-success']");
|
||||||
|
|
||||||
await expect(toast).toBeTruthy();
|
expect(toast).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Loaded 100 of 367 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user