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 | ||
|
|
4803817055 | ||
|
|
dac6bff5b1 | ||
|
|
a3c10d0beb | ||
|
|
d2b45e52ff | ||
|
|
0da6878188 | ||
|
|
572b54b7f0 | ||
|
|
d9bf89beab | ||
|
|
0d1f484c92 | ||
|
|
51d02c9873 | ||
|
|
6226baa244 | ||
|
|
81d842ade7 | ||
|
|
d26732c9dc | ||
|
|
f5318c6be3 | ||
|
|
dafbebe6f3 | ||
|
|
4adfcb4b6b | ||
|
|
3ac6a8aa39 | ||
|
|
c8019edb57 | ||
|
|
bfc81b770a | ||
|
|
4a5404bf21 | ||
|
|
7a9b40f218 |
No files matched your search
@@ -0,0 +1,33 @@
|
|||||||
|
# This is a comment.
|
||||||
|
# Each line is a file pattern followed by one or more owners.
|
||||||
|
|
||||||
|
# These owners will be the default owners for everything in
|
||||||
|
# the repo. Unless a later match takes precedence,
|
||||||
|
# @global-owner1 and @global-owner2 will be requested for
|
||||||
|
# review when someone opens a pull request.
|
||||||
|
* @calcom/reviewers
|
||||||
|
|
||||||
|
.github/ @calcom/core
|
||||||
|
.husky/ @calcom/core
|
||||||
|
.vcode/ @calcom/core
|
||||||
|
.snaplet/ @calcom/core
|
||||||
|
deploy/ @calcom/core
|
||||||
|
scripts/ @calcom/core
|
||||||
|
|
||||||
|
apps/docs @calcom/docs
|
||||||
|
apps/swagger @calcom/docs
|
||||||
|
|
||||||
|
packages/app-store-cli @hariombalhara @calcom/core
|
||||||
|
packages/dayjs @zomars @calcom/core
|
||||||
|
packages/emails @zomars @calcom/core
|
||||||
|
packages/embeds @hariombalhara @calcom/core
|
||||||
|
packages/eslint-plugin @zomars @calcom/core
|
||||||
|
packages/features @zomars @calcom/core
|
||||||
|
packages/features/kbar @alishaz-polymath @calcom/core
|
||||||
|
packages/features/ee/impersonation @sean-brydon @calcom/core
|
||||||
|
packages/features/ee/payments @alannnc @calcom/core
|
||||||
|
packages/features/ee/workflows @CarinaWolli @calcom/core
|
||||||
|
packages/features/tips @PeerRich @calcom/core
|
||||||
|
packages/ui @calcom/ui
|
||||||
|
packages/config @calcom/core
|
||||||
|
packages/app-store/ee/routing-forms @hariombalhara @calcom/core
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
name: Cache production build binaries
|
||||||
|
description: "Cache or restore if necessary"
|
||||||
|
inputs:
|
||||||
|
node_version:
|
||||||
|
required: false
|
||||||
|
default: v16.x
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Cache production build
|
||||||
|
uses: actions/cache@v3
|
||||||
|
id: cache-build
|
||||||
|
env:
|
||||||
|
cache-name: prod-build
|
||||||
|
key-1: ${{ inputs.node_version }}-${{ hashFiles('yarn.lock') }}
|
||||||
|
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:
|
||||||
|
path: |
|
||||||
|
${{ github.workspace }}/apps/web/.next
|
||||||
|
**/.turbo/**
|
||||||
|
**/dist/**
|
||||||
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||||
|
- run: yarn build
|
||||||
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
|
shell: bash
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name: Cache database between jobs
|
||||||
|
description: "Cache or restore if necessary"
|
||||||
|
inputs:
|
||||||
|
DATABASE_URL:
|
||||||
|
required: false
|
||||||
|
default: "postgresql://postgres:@localhost:5432/calendso"
|
||||||
|
path:
|
||||||
|
required: false
|
||||||
|
default: "backups/backup.sql"
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Cache database
|
||||||
|
id: cache-db
|
||||||
|
uses: actions/cache@v3
|
||||||
|
env:
|
||||||
|
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:
|
||||||
|
path: ${{ inputs.path }}
|
||||||
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}
|
||||||
|
- run: yarn db-seed
|
||||||
|
if: steps.cache-db.outputs.cache-hit != 'true'
|
||||||
|
shell: bash
|
||||||
|
- name: Postgres Dump Backup
|
||||||
|
if: steps.cache-db.outputs.cache-hit != 'true'
|
||||||
|
uses: tj-actions/pg-dump@v2.3
|
||||||
|
with:
|
||||||
|
database_url: ${{ inputs.DATABASE_URL }}
|
||||||
|
path: ${{ inputs.path }}
|
||||||
|
options: "-O"
|
||||||
|
- name: Postgres Backup Restore
|
||||||
|
if: steps.cache-db.outputs.cache-hit == 'true'
|
||||||
|
uses: tj-actions/pg-restore@v4.5
|
||||||
|
with:
|
||||||
|
database_url: ${{ inputs.DATABASE_URL }}
|
||||||
|
backup_file: ${{ inputs.path }}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
name: Dangerous git Checkout
|
||||||
|
description: "Git Checkout from PR code so we can run checks from forks"
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
fetch-depth: 2
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: Yarn install
|
||||||
|
description: "Install all NPM dependencies, caches them and restores if necessary"
|
||||||
|
inputs:
|
||||||
|
node_version:
|
||||||
|
required: false
|
||||||
|
default: v16.x
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Use Node ${{ inputs.node_version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ inputs.node_version }}
|
||||||
|
cache: "yarn"
|
||||||
|
- name: Yarn install
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
yarn install --prefer-offline --frozen-lockfile
|
||||||
|
yarn prisma generate
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: Install playwright binaries
|
||||||
|
description: "Install playwright, cache and restore if necessary"
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Cache playwright binaries
|
||||||
|
id: playwright-cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/Library/Caches/ms-playwright
|
||||||
|
~/.cache/ms-playwright
|
||||||
|
${{ github.workspace }}/node_modules/playwright
|
||||||
|
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: cache-playwright-
|
||||||
|
- name: Yarn playwright install
|
||||||
|
shell: bash
|
||||||
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
|
run: yarn playwright install
|
||||||
@@ -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 }}
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|||||||
@@ -1,34 +1,17 @@
|
|||||||
name: Check types
|
name: Check types
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
workflow_call:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- "**.tsx?"
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max-old-space-size=8192"
|
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||||
jobs:
|
jobs:
|
||||||
check-types:
|
check-types:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
with:
|
- uses: ./.github/actions/yarn-install
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Use Node 16.x
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
|
|
||||||
with:
|
|
||||||
node-version: 16.x
|
|
||||||
cache: "yarn"
|
|
||||||
|
|
||||||
- name: Show info
|
- name: Show info
|
||||||
run: node -e "console.log(require('v8').getHeapStatistics())"
|
run: node -e "console.log(require('v8').getHeapStatistics())"
|
||||||
|
|
||||||
- run: yarn --frozen-lockfile
|
|
||||||
- run: yarn type-check
|
- run: yarn type-check
|
||||||
@@ -9,14 +9,12 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
required-checks:
|
|
||||||
uses: calcom/cal.com/.github/workflows/required-checks.yml@main
|
|
||||||
synchronize-with-crowdin:
|
synchronize-with-crowdin:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: crowdin action
|
- name: crowdin action
|
||||||
uses: crowdin/github-action@1.4.9
|
uses: crowdin/github-action@1.4.9
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ jobs:
|
|||||||
db-staging-snapshot:
|
db-staging-snapshot:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Install Snaplet CLI
|
- name: Install Snaplet CLI
|
||||||
run: curl -sL https://app.snaplet.dev/get-cli/ | bash
|
run: curl -sL https://app.snaplet.dev/get-cli/ | bash
|
||||||
- name: Restore Snapshot
|
- name: Restore Snapshot
|
||||||
|
|||||||
@@ -1,60 +1,12 @@
|
|||||||
name: E2E App-Store Apps
|
name: E2E App-Store Apps
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_call:
|
||||||
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
|
||||||
pull_request_target: # So we can test on forks
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths-ignore:
|
|
||||||
- apps/api/**
|
|
||||||
- apps/console/**
|
|
||||||
- apps/docs/**
|
|
||||||
- apps/swagger/**
|
|
||||||
- apps/website/**
|
|
||||||
- apps/web/public/**
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
e2e-app-store:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
name: E2E App-Store Apps
|
name: E2E App-Store Apps
|
||||||
strategy:
|
runs-on: ubuntu-latest
|
||||||
matrix:
|
|
||||||
node: ["16.x"]
|
|
||||||
os: [ubuntu-latest]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
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
|
||||||
@@ -65,35 +17,18 @@ jobs:
|
|||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
|
||||||
fetch-depth: 2
|
|
||||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||||
- name: Use Node ${{ matrix.node }}
|
- uses: ./.github/actions/yarn-install
|
||||||
uses: actions/setup-node@v3
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
with:
|
- uses: ./.github/actions/env-read-file
|
||||||
node-version: ${{ matrix.node }}
|
- uses: ./.github/actions/cache-db
|
||||||
cache: "yarn"
|
- uses: ./.github/actions/cache-build
|
||||||
|
|
||||||
- name: Cache playwright binaries
|
|
||||||
uses: actions/cache@v2
|
|
||||||
id: playwright-cache
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/Library/Caches/ms-playwright
|
|
||||||
~/.cache/ms-playwright
|
|
||||||
${{ github.workspace }}/node_modules/playwright
|
|
||||||
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: cache-playwright-
|
|
||||||
- run: yarn --frozen-lockfile
|
|
||||||
- name: Install playwright deps
|
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
||||||
run: yarn playwright install --with-deps
|
|
||||||
- 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
|
||||||
|
|||||||
@@ -1,56 +1,11 @@
|
|||||||
name: E2E Embed tests and booking flow(for non-embed as well)
|
name: E2E Embed tests and booking flow(for non-embed as well)
|
||||||
on:
|
on:
|
||||||
pull_request_target: # So we can test on forks
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
# Embed e2e - tests verify booking flow which is applicable to non-embed case also. So, don't ignore apps/web changes.
|
|
||||||
paths:
|
|
||||||
- apps/web/**
|
|
||||||
- packages/embeds/**
|
|
||||||
workflow_call:
|
workflow_call:
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
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
|
||||||
@@ -61,45 +16,28 @@ jobs:
|
|||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
|
||||||
fetch-depth: 2
|
|
||||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||||
- name: Use Node 16.x
|
- uses: ./.github/actions/yarn-install
|
||||||
uses: actions/setup-node@v3
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
with:
|
- uses: ./.github/actions/env-read-file
|
||||||
node-version: 16.x
|
- uses: ./.github/actions/cache-db
|
||||||
cache: "yarn"
|
- uses: ./.github/actions/cache-build
|
||||||
|
|
||||||
- name: Cache playwright binaries
|
|
||||||
uses: actions/cache@v2
|
|
||||||
id: playwright-cache
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/Library/Caches/ms-playwright
|
|
||||||
~/.cache/ms-playwright
|
|
||||||
${{ github.workspace }}/node_modules/playwright
|
|
||||||
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: cache-playwright-
|
|
||||||
- run: yarn --frozen-lockfile
|
|
||||||
- name: Install playwright deps
|
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
||||||
run: yarn playwright install --with-deps
|
|
||||||
- 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
|
||||||
+14
-79
@@ -1,59 +1,11 @@
|
|||||||
name: E2E test
|
name: E2E test
|
||||||
on:
|
on:
|
||||||
pull_request_target: # So we can test on forks
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- apps/web/**
|
|
||||||
- packages/**
|
|
||||||
workflow_call:
|
workflow_call:
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
jobs:
|
||||||
e2e:
|
e2e:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
name: E2E tests
|
||||||
|
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 }}
|
|
||||||
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
|
||||||
@@ -62,37 +14,20 @@ jobs:
|
|||||||
POSTGRES_DB: calendso
|
POSTGRES_DB: calendso
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
|
||||||
fetch-depth: 2
|
|
||||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||||
- name: Use Node 16.x
|
- uses: ./.github/actions/yarn-install
|
||||||
uses: actions/setup-node@v3
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
with:
|
- uses: ./.github/actions/env-read-file
|
||||||
node-version: 16.x
|
- uses: ./.github/actions/cache-db
|
||||||
cache: "yarn"
|
- uses: ./.github/actions/cache-build
|
||||||
|
- name: Run Tests
|
||||||
- name: Cache playwright binaries
|
run: yarn test-e2e
|
||||||
uses: actions/cache@v2
|
env:
|
||||||
id: playwright-cache
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
with:
|
- name: Upload Test Results
|
||||||
path: |
|
|
||||||
~/Library/Caches/ms-playwright
|
|
||||||
~/.cache/ms-playwright
|
|
||||||
${{ github.workspace }}/node_modules/playwright
|
|
||||||
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: cache-playwright-
|
|
||||||
- run: yarn --frozen-lockfile
|
|
||||||
- name: Install playwright deps
|
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
||||||
run: yarn playwright install --with-deps
|
|
||||||
- run: yarn test-e2e
|
|
||||||
|
|
||||||
- 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') }}
|
||||||
@@ -1,38 +1,14 @@
|
|||||||
name: Lint
|
name: Lint
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
workflow_call:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- "**.tsx?"
|
|
||||||
- "**.jsx?"
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
with:
|
- uses: ./.github/actions/yarn-install
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Use Node 16.x
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 16.x
|
|
||||||
cache: "yarn"
|
|
||||||
|
|
||||||
- name: Install deps
|
|
||||||
if: steps.yarn-cache.outputs.cache-hit != 'true'
|
|
||||||
run: yarn --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Test Code Linting
|
|
||||||
run: yarn lint
|
|
||||||
|
|
||||||
- name: Save Code Linting Reports
|
- name: Save Code Linting Reports
|
||||||
run: yarn lint:report
|
run: yarn lint:report
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|||||||
@@ -1,91 +1,30 @@
|
|||||||
name: "Next.js Bundle Analysis"
|
name: "Next.js Bundle Analysis"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
workflow_call:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
# change this if your nextjs app does not live at the root of the repo
|
|
||||||
working-directory: ./apps/web/
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Production build
|
||||||
|
if: ${{ github.event_name == 'push' }}
|
||||||
|
uses: ./.github/workflows/production-build.yml
|
||||||
|
secrets: inherit
|
||||||
analyze:
|
analyze:
|
||||||
env:
|
needs: build
|
||||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
if: always()
|
||||||
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
|
|
||||||
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 }}
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:12.1
|
|
||||||
env:
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_DB: calendso
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- name: Set up node
|
- uses: ./.github/actions/yarn-install
|
||||||
uses: actions/setup-node@v3
|
- uses: ./.github/actions/cache-build
|
||||||
with:
|
|
||||||
node-version: 16.x
|
|
||||||
cache: "yarn"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
uses: bahmutov/npm-install@v1
|
|
||||||
|
|
||||||
- name: Restore next build
|
|
||||||
uses: actions/cache@v2
|
|
||||||
id: restore-build-cache
|
|
||||||
env:
|
|
||||||
cache-name: cache-next-build
|
|
||||||
with:
|
|
||||||
path: apps/web/.next/cache
|
|
||||||
# change this if you prefer a more strict cache
|
|
||||||
key: ${{ runner.os }}-build-${{ env.cache-name }}
|
|
||||||
|
|
||||||
- name: Build next.js app
|
|
||||||
# change this if your site requires a custom build command
|
|
||||||
run: cd ../.. && yarn db-seed && yarn build
|
|
||||||
|
|
||||||
# Here's the first place where next-bundle-analysis' own script is used
|
|
||||||
# This step pulls the raw bundle stats for the current bundle
|
|
||||||
- name: Analyze bundle
|
- name: Analyze bundle
|
||||||
run: npx -p nextjs-bundle-analysis report
|
run: |
|
||||||
|
cd apps/web
|
||||||
|
npx -p nextjs-bundle-analysis report
|
||||||
|
|
||||||
- name: Upload bundle
|
- name: Upload bundle
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
@@ -116,12 +55,15 @@ jobs:
|
|||||||
# entry in your package.json file.
|
# entry in your package.json file.
|
||||||
- name: Compare with base branch bundle
|
- name: Compare with base branch bundle
|
||||||
if: success() && github.event.number
|
if: success() && github.event.number
|
||||||
run: ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare
|
run: |
|
||||||
|
cd apps/web
|
||||||
|
ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare
|
||||||
|
|
||||||
- name: Get comment body
|
- name: Get comment body
|
||||||
id: get-comment-body
|
id: get-comment-body
|
||||||
if: success() && github.event.number
|
if: success() && github.event.number
|
||||||
run: |
|
run: |
|
||||||
|
cd apps/web
|
||||||
body=$(cat .next/analyze/__bundle_analysis_comment.txt)
|
body=$(cat .next/analyze/__bundle_analysis_comment.txt)
|
||||||
body="${body//'%'/'%25'}"
|
body="${body//'%'/'%25'}"
|
||||||
body="${body//$'\n'/'%0A'}"
|
body="${body//$'\n'/'%0A'}"
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
name: PR Update
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- "**.md"
|
||||||
|
- ".github/CODEOWNERS"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
changes:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
outputs:
|
||||||
|
app-store: ${{ steps.filter.outputs.app-store }}
|
||||||
|
embed: ${{ steps.filter.outputs.embed }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
- uses: dorny/paths-filter@v2
|
||||||
|
id: filter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
app-store:
|
||||||
|
- 'apps/web/**'
|
||||||
|
- 'packages/app-store/**'
|
||||||
|
embed:
|
||||||
|
- 'apps/web/**'
|
||||||
|
- 'packages/embeds/**'
|
||||||
|
|
||||||
|
env:
|
||||||
|
name: Create env file
|
||||||
|
uses: ./.github/workflows/env-create-file.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
type-check:
|
||||||
|
name: Type check
|
||||||
|
uses: ./.github/workflows/check-types.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Unit tests
|
||||||
|
uses: ./.github/workflows/test.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Linters
|
||||||
|
uses: ./.github/workflows/lint.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Production build
|
||||||
|
needs: env
|
||||||
|
uses: ./.github/workflows/production-build.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e:
|
||||||
|
name: E2E tests
|
||||||
|
needs: [changes, lint, build]
|
||||||
|
uses: ./.github/workflows/e2e.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-app-store:
|
||||||
|
name: E2E App Store tests
|
||||||
|
if: ${{ needs.changes.outputs.app-store == 'true' }}
|
||||||
|
needs: [changes, lint, build]
|
||||||
|
uses: ./.github/workflows/e2e-app-store.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-embed:
|
||||||
|
name: E2E embeds tests
|
||||||
|
if: ${{ needs.changes.outputs.embed == 'true' }}
|
||||||
|
needs: [changes, lint, build]
|
||||||
|
uses: ./.github/workflows/e2e-embed.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
analyze:
|
||||||
|
needs: build
|
||||||
|
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||||
|
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
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: Production Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:12.1
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_DB: calendso
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
- uses: ./.github/actions/yarn-install
|
||||||
|
- uses: ./.github/actions/env-read-file
|
||||||
|
- uses: ./.github/actions/cache-db
|
||||||
|
- uses: ./.github/actions/cache-build
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
name: "Meta Workflow: Require Conditional Status Checks"
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_call:
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
|
||||||
required-checks:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Ensure All Conditional Checks Have Passed
|
|
||||||
uses: blend/require-conditional-status-checks@2022.02.04
|
|
||||||
with:
|
|
||||||
timeout: 15m
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
interval: 20s
|
|
||||||
# Question marks aren't working
|
|
||||||
# @see https://github.com/blend/require-conditional-status-checks/pull/11
|
|
||||||
checks-yaml: |
|
|
||||||
- job: lint
|
|
||||||
paths:
|
|
||||||
- /**/*.jsx
|
|
||||||
- /**/*.js
|
|
||||||
- /**/*.tsx
|
|
||||||
- /**/*.ts
|
|
||||||
- job: check-types
|
|
||||||
paths:
|
|
||||||
- /**/*.tsx
|
|
||||||
- /**/*.ts
|
|
||||||
- job: e2e
|
|
||||||
paths:
|
|
||||||
- /apps/web/**
|
|
||||||
- /packages/**
|
|
||||||
- job: e2e-embed
|
|
||||||
paths:
|
|
||||||
- /apps/web/**
|
|
||||||
- /packages/embeds/**
|
|
||||||
- job: test
|
|
||||||
paths:
|
|
||||||
- /apps/web/**
|
|
||||||
- /packages/**
|
|
||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
|||||||
@@ -1,33 +1,17 @@
|
|||||||
name: Unit tests
|
name: Unit tests
|
||||||
on:
|
on:
|
||||||
pull_request_target: # So we can test on forks
|
workflow_call:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- apps/web/**
|
|
||||||
- packages/**
|
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows: [Crowdin Action]
|
workflows: [Crowdin Action]
|
||||||
types: [completed]
|
types: [completed]
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
|
||||||
fetch-depth: 2
|
|
||||||
# Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
|
|
||||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=6144"' >> $GITHUB_ENV
|
- run: echo 'NODE_OPTIONS="--max_old_space_size=6144"' >> $GITHUB_ENV
|
||||||
- name: Use Node 16.x
|
- uses: ./.github/actions/yarn-install
|
||||||
uses: actions/setup-node@v3
|
# Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
|
||||||
with:
|
|
||||||
node-version: 16.x
|
|
||||||
cache: "yarn"
|
|
||||||
- run: yarn --frozen-lockfile
|
|
||||||
- run: yarn test
|
- run: yarn test
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
name: Yarn install
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
setup:
|
||||||
|
name: Yarn install & cache
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
- uses: ./.github/actions/yarn-install
|
||||||
Vendored
+12
-30
@@ -2,38 +2,20 @@
|
|||||||
"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.
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://cal.com/slack"><img src="https://img.shields.io/badge/Slack-calendso.slack.com-%234A154B" alt="Join Cal.com Slack"></a>
|
<a href="https://cal.com/slack"><img src="https://img.shields.io/badge/Slack-calendso.slack.com-%234A154B" alt="Join Cal.com Slack"></a>
|
||||||
<a href="https://www.producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
|
<a href="https://www.producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
|
||||||
<a href="https://status.cal.com"><img src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a>
|
<a href="https://status.cal.com"><img height="20px" src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a>
|
||||||
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
|
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
|
||||||
<a href="https://news.ycombinator.com/item?id=34507672"><img src="https://img.shields.io/badge/Hacker%20News-%231-%23FF6600" alt="Hacker News"></a>
|
<a href="https://news.ycombinator.com/item?id=34507672"><img src="https://img.shields.io/badge/Hacker%20News-%231-%23FF6600" alt="Hacker News"></a>
|
||||||
<a href="https://github.com/calcom/cal.com/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
|
<a href="https://github.com/calcom/cal.com/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
|
||||||
@@ -350,8 +350,17 @@ We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:is
|
|||||||
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
|
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
|
||||||
</picture>
|
</picture>
|
||||||
</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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Tooltip content={t(locationKeyToString(location) ?? "")}>
|
<Tooltip content={t(locationKeyToString(location) ?? "")}>
|
||||||
<p className="truncate">{t(locationKeyToString(location) ?? "")}</p>
|
<p className="line-clamp-1">{t(locationKeyToString(location) ?? "")}</p>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 = {
|
||||||
@@ -111,7 +121,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-primary-500 dark:bg-darkgray-200 dark:text-darkgray-900 mb-2 block rounded-sm border bg-white py-2 font-medium opacity-25 dark:border-transparent ",
|
"text-primary-500 dark:bg-darkgray-200 dark:text-darkgray-900 mb-2 block rounded-sm border bg-white py-2 font-medium opacity-25 dark:border-transparent ",
|
||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
)}>
|
)}>
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
@@ -122,8 +132,8 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
href={bookingUrl}
|
href={bookingUrl}
|
||||||
prefetch={false}
|
prefetch={false}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-primary-500 hover:border-gray-900 hover:bg-gray-50",
|
"hover:bg-brand hover:border-brand hover:text-brandcontrast dark:hover:text-darkmodebrandcontrast",
|
||||||
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand dark:text-darkgray-800 mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent",
|
"dark:bg-darkgray-200 dark:hover:bg-darkmodebrand dark:hover:border-darkmodebrand dark:text-darkgray-800 mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent",
|
||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
)}
|
)}
|
||||||
data-testid="time">
|
data-testid="time">
|
||||||
|
|||||||
@@ -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} />;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -188,6 +191,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
control={locationFormMethods.control}
|
control={locationFormMethods.control}
|
||||||
render={() => (
|
render={() => (
|
||||||
<CheckboxField
|
<CheckboxField
|
||||||
|
data-testid="display-location"
|
||||||
defaultChecked={defaultLocation?.displayLocationPublicly}
|
defaultChecked={defaultLocation?.displayLocationPublicly}
|
||||||
description={t("display_location_label")}
|
description={t("display_location_label")}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -345,7 +349,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button type="submit">{t("update")}</Button>
|
<Button data-testid="update-location" type="submit">
|
||||||
|
{t("update")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -197,14 +202,14 @@ export const EventSetupTab = (
|
|||||||
<li
|
<li
|
||||||
key={`${location.type}${index}`}
|
key={`${location.type}${index}`}
|
||||||
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
||||||
<div className="flex max-w-full justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div key={index} className="flex flex-grow items-center">
|
<div className="flex items-center">
|
||||||
<img
|
<img
|
||||||
src={eventLocationType.iconUrl}
|
src={eventLocationType.iconUrl}
|
||||||
className="h-4 w-4"
|
className="h-4 w-4"
|
||||||
alt={`${eventLocationType.label} logo`}
|
alt={`${eventLocationType.label} logo`}
|
||||||
/>
|
/>
|
||||||
<span className="truncate text-sm ltr:ml-1 rtl:mr-1">{eventLabel}</span>
|
<span className="line-clamp-1 text-sm ltr:ml-1 rtl:mr-1">{eventLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<button
|
<button
|
||||||
@@ -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,14 +249,17 @@ 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 StartIcon={FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}>
|
<Button
|
||||||
|
data-testid="add-location"
|
||||||
|
StartIcon={FiPlus}
|
||||||
|
color="minimal"
|
||||||
|
onClick={() => setShowLocationModal(true)}>
|
||||||
{t("add_location")}
|
{t("add_location")}
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
@@ -268,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",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export default function LocationSelect(props: Props<LocationOption, false, Group
|
|||||||
return (
|
return (
|
||||||
<Select<LocationOption>
|
<Select<LocationOption>
|
||||||
name="location"
|
name="location"
|
||||||
|
id="location-select"
|
||||||
components={{
|
components={{
|
||||||
Option: (props) => (
|
Option: (props) => (
|
||||||
<components.Option {...props}>
|
<components.Option {...props}>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -3,6 +3,7 @@ import type { IncomingMessage, OutgoingMessage } from "http";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||||
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
|
||||||
function getCspPolicy(nonce: string) {
|
function getCspPolicy(nonce: string) {
|
||||||
//TODO: Do we need to explicitly define it in turbo.json
|
//TODO: Do we need to explicitly define it in turbo.json
|
||||||
@@ -12,7 +13,12 @@ function getCspPolicy(nonce: string) {
|
|||||||
// We can remove 'unsafe-inline' from style-src when we add nonces to all style tags
|
// We can remove 'unsafe-inline' from style-src when we add nonces to all style tags
|
||||||
// Maybe see how @next-safe/middleware does it if it's supported.
|
// Maybe see how @next-safe/middleware does it if it's supported.
|
||||||
const useNonStrictPolicy = CSP_POLICY === "non-strict";
|
const useNonStrictPolicy = CSP_POLICY === "non-strict";
|
||||||
|
const SENTRY_ENDPOINT = process.env.NEXT_PUBLIC_SENTRY_DSN
|
||||||
|
? new URL(process.env.NEXT_PUBLIC_SENTRY_DSN, "http://base_url").origin
|
||||||
|
: "";
|
||||||
|
|
||||||
|
// We add WEBAPP_URL to img-src because of booking pages, which end up loading images from app.cal.com on cal.com
|
||||||
|
// FIXME: Write a layer to extract out EventType Analytics tracking endpoints and add them to img-src or connect-src as needed. e.g. fathom, Google Analytics and others
|
||||||
return `
|
return `
|
||||||
default-src 'self' ${IS_PRODUCTION ? "" : "data:"};
|
default-src 'self' ${IS_PRODUCTION ? "" : "data:"};
|
||||||
script-src ${
|
script-src ${
|
||||||
@@ -29,7 +35,8 @@ function getCspPolicy(nonce: string) {
|
|||||||
IS_PRODUCTION ? (useNonStrictPolicy ? "'unsafe-inline'" : "") : "'unsafe-inline'"
|
IS_PRODUCTION ? (useNonStrictPolicy ? "'unsafe-inline'" : "") : "'unsafe-inline'"
|
||||||
} app.cal.com;
|
} app.cal.com;
|
||||||
font-src 'self';
|
font-src 'self';
|
||||||
img-src 'self' https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:
|
img-src 'self' ${WEBAPP_URL} https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:;
|
||||||
|
connect-src 'self' ${SENTRY_ENDPOINT}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.3",
|
"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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const getStaticProps: GetStaticProps<
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const startDate = (
|
const startDate = (
|
||||||
dayjs(month, "YYYY-MM").isSame(dayjs(), "month") ? dayjs() : dayjs(month, "YYYY-MM")
|
dayjs(month, "YYYY-MM").isSame(dayjs(), "month") ? dayjs.utc() : dayjs.utc(month, "YYYY-MM")
|
||||||
).startOf("day");
|
).startOf("day");
|
||||||
const endDate = startDate.endOf("month");
|
const endDate = startDate.endOf("month");
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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,48 +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.union([
|
|
||||||
z.object({
|
|
||||||
download_link: z.string(),
|
|
||||||
expires: z.number(),
|
|
||||||
}),
|
|
||||||
z.object({}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const requestQuery = z.object({
|
|
||||||
recordingId: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<z.infer<typeof getAccessLinkSchema> | void>
|
|
||||||
) {
|
|
||||||
const { recordingId } = requestQuery.parse(req.query);
|
|
||||||
const session = await getSession({ req });
|
|
||||||
|
|
||||||
// Check if user belong to active team
|
|
||||||
const isDownloadAllowed = IS_SELF_HOSTED ? true : !session?.user?.belongsToActiveTeam;
|
|
||||||
if (!isDownloadAllowed) {
|
|
||||||
return res.status(403);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const response = await fetcher(`/recordings/${recordingId}/access-link`).then(getAccessLinkSchema.parse);
|
|
||||||
|
|
||||||
if ("download_link" in response && response.download_link) {
|
|
||||||
return res.status(200).json(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(400);
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
||||||
@@ -374,6 +371,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
<ButtonGroup combined>
|
<ButtonGroup combined>
|
||||||
<Tooltip content={t("preview")}>
|
<Tooltip content={t("preview")}>
|
||||||
<Button
|
<Button
|
||||||
|
data-testid="preview-link-button"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
@@ -630,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)");
|
||||||
|
|
||||||
@@ -686,6 +704,7 @@ const EventTypesPage = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<EmbedDialog />
|
<EmbedDialog />
|
||||||
|
{router.query.dialog === "duplicate" && <DuplicateDialog />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -694,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(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Loaded 100 of 389 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user