Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4803817055 | ||
|
|
dac6bff5b1 | ||
|
|
a3c10d0beb | ||
|
|
d2b45e52ff | ||
|
|
0da6878188 | ||
|
|
572b54b7f0 | ||
|
|
d9bf89beab | ||
|
|
0d1f484c92 | ||
|
|
51d02c9873 | ||
|
|
6226baa244 | ||
|
|
81d842ade7 | ||
|
|
d26732c9dc | ||
|
|
f5318c6be3 | ||
|
|
dafbebe6f3 | ||
|
|
4adfcb4b6b | ||
|
|
3ac6a8aa39 | ||
|
|
c8019edb57 | ||
|
|
bfc81b770a | ||
|
|
4a5404bf21 | ||
|
|
7a9b40f218 | ||
|
|
dca1d35cb4 | ||
|
|
af970c049a | ||
|
|
6fc3840ca3 | ||
|
|
c51150350a | ||
|
|
51664bf733 | ||
|
|
831506447a | ||
|
|
ee54e670cb | ||
|
|
99cd6fe7da | ||
|
|
75c7e03efe | ||
|
|
2ebfcf448c | ||
|
|
235ec0e906 | ||
|
|
ea73870ce9 | ||
|
|
d1440bb5d2 | ||
|
|
a000030040 | ||
|
|
44864a801d | ||
|
|
b9fd0b4c78 | ||
|
|
0e7276370f | ||
|
|
4d2cf28c7e | ||
|
|
55ee939cf9 | ||
|
|
2fe54ded15 | ||
|
|
8e3fa4be29 | ||
|
|
d7b4d5932d | ||
|
|
3c7fc85b34 | ||
|
|
af018f0e8b | ||
|
|
6573f75a83 | ||
|
|
a3b26cd1e7 | ||
|
|
4f99746f68 | ||
|
|
fd76bc9cab | ||
|
|
602c813928 | ||
|
|
2c54ea90f9 | ||
|
+17 |
2698cde311 | ||
|
|
1433ec6946 | ||
|
|
48e5d18ea1 | ||
|
|
5da76c698e | ||
|
|
578308a8cb | ||
|
|
6b3862f96d | ||
|
|
94f239bc42 | ||
|
|
2f2f6006e6 | ||
|
|
f8558e16e8 | ||
|
|
38be2bef9a | ||
|
|
485619602f | ||
|
|
ce01537fe6 | ||
|
|
f77e7476d2 | ||
|
|
b6016c8f20 | ||
|
|
a5e5d103a0 | ||
|
|
fec02979ba | ||
|
|
dc200b95cb | ||
|
|
78a54e5f54 | ||
|
|
00ebda2fc6 | ||
|
|
7c749299bb | ||
|
|
3df289b557 | ||
|
|
f4c849b75c | ||
|
|
09cc6f07e4 | ||
|
|
78656f2116 | ||
|
|
1fa5e296af | ||
|
|
0f76530c11 | ||
|
|
ab6758194b | ||
|
|
5528d60897 | ||
|
|
bb1c55e07f | ||
|
|
c899921591 | ||
|
|
ea34545d72 | ||
|
|
9d3540511b | ||
|
|
e8cf92361a | ||
|
|
e8525b963b | ||
|
|
24dabd0ddc | ||
|
|
5203168b2e | ||
|
|
c3e40f84a9 | ||
|
|
8e2ece7526 | ||
|
|
b6eca6e083 | ||
|
|
4cb264f1f7 | ||
|
|
e248fda36b | ||
|
|
1c97e549eb | ||
|
|
77600f8168 | ||
|
|
6cad606ba1 | ||
|
|
7cae7a7bd4 | ||
|
|
93f65807e7 | ||
|
|
0a63820699 | ||
|
|
05ad437b27 | ||
|
|
26a57a71ae | ||
|
|
34a10205a3 | ||
|
|
fbf8ff6db7 | ||
|
|
bef6d18fdf | ||
|
|
2e96e2ef3f | ||
|
|
2ca6769a70 | ||
|
|
1eb514dfcf | ||
|
|
d6c004ba22 | ||
|
+17 |
afb9605f0a | ||
|
|
a2549bd7b6 | ||
|
|
c2a8f799ff | ||
|
|
c68ff54e77 | ||
|
|
74fe20802d | ||
|
|
d59cda3286 | ||
|
|
1cae2b6f3d | ||
|
|
9d9fa8e575 | ||
|
|
f04c634c48 | ||
|
|
c3e7981917 | ||
|
|
d6594dab0f | ||
|
|
5fcfbb2d90 | ||
|
|
8a99c3d2a5 | ||
|
|
336c2d3643 | ||
|
|
cfe3ef4ab0 | ||
|
|
4ec8241a86 | ||
|
|
e6313aafac | ||
|
|
f87f030bb5 | ||
|
|
f87254cdc3 | ||
|
|
1f74837189 | ||
|
|
c7064906c9 | ||
|
|
86c94a3cd5 | ||
|
|
2056ebe186 | ||
|
|
53424ca9ae | ||
|
|
8197ba18d4 | ||
|
|
d341ea5111 | ||
|
|
42dcd4a2a9 | ||
|
|
c63225fdbf | ||
|
|
41774e4932 | ||
|
|
af7849f951 | ||
|
|
5353610d9a | ||
|
+14 |
156f3e5de2 | ||
|
|
05f51459a6 | ||
|
|
f0db97bbb1 | ||
|
|
05c33bd94b | ||
|
|
7d4cc59cd2 | ||
|
|
a94465ff04 | ||
|
|
4d54ac3ca2 | ||
|
|
15da690270 | ||
|
|
2462e8e597 | ||
|
|
2f628a17df | ||
|
|
1ee3783db3 | ||
|
|
b3d8d7e944 | ||
|
|
a4b7f1af65 | ||
|
|
f7445bf8a1 | ||
|
|
9cad7086d7 | ||
|
|
3345ec61ee | ||
|
|
7797421ce9 | ||
|
|
075a274207 | ||
|
|
a46a31040a | ||
|
|
7a28e1d6f4 | ||
|
|
51fd7fdb3e | ||
|
|
d01963303a | ||
|
|
6877d0f693 | ||
|
|
39040fca9a | ||
|
|
6fa4befc9b | ||
|
|
8c9871c6ce | ||
|
+8 |
a9af2fb255 | ||
|
|
8c150264f4 | ||
|
|
cfc3644d51 | ||
|
|
bdb4a7353b | ||
|
|
30c0e6d1d7 | ||
|
|
390967990a | ||
|
|
cc56602474 | ||
|
|
43a044d59e | ||
|
|
5b8ed9c3ef | ||
|
|
c3d6df2dcf | ||
|
|
9d02cb493c | ||
|
|
26ed478e50 | ||
|
|
09e338970d | ||
|
|
f60399ca8b | ||
|
|
5cf7e32852 | ||
|
|
24be8a63f2 | ||
|
|
75509c97fd | ||
|
|
9b26c7aee6 | ||
|
|
f18e80cfcf | ||
|
|
0a4b1ca014 | ||
|
|
9fa6aced33 | ||
|
|
9093909075 | ||
|
|
832fce91e0 | ||
|
|
6da41fce63 | ||
|
|
3b51cc9aef | ||
|
|
50c8959cfa | ||
|
|
c96d258f5e | ||
|
|
6e45b1c383 | ||
|
|
551ebe863b | ||
|
|
fa7b94bfe9 | ||
|
|
56c5d39cd3 | ||
|
|
7574fb6f44 | ||
|
|
901d1f0e01 | ||
|
|
828fcbe1d9 | ||
|
|
34db46869e | ||
|
|
1f1cab2be1 |
No files matched your search
+11
-4
@@ -1,20 +1,21 @@
|
|||||||
# ********** INDEX **********
|
# ********** INDEX **********
|
||||||
#
|
#
|
||||||
# - LICENSE
|
# - LICENSE (DEPRECATED)
|
||||||
# - DATABASE
|
# - DATABASE
|
||||||
# - SHARED
|
# - SHARED
|
||||||
# - NEXTAUTH
|
# - NEXTAUTH
|
||||||
# - E-MAIL SETTINGS
|
# - E-MAIL SETTINGS
|
||||||
|
|
||||||
# - LICENSE *************************************************************************************************
|
# - LICENSE (DEPRECATED) ************************************************************************************
|
||||||
# https://github.com/calendso/calendso/blob/main/LICENSE
|
# https://github.com/calcom/cal.com/blob/main/LICENSE
|
||||||
#
|
#
|
||||||
# Summary of terms:
|
# Summary of terms:
|
||||||
# - The codebase has to stay open source, whether it was modified or not
|
# - The codebase has to stay open source, whether it was modified or not
|
||||||
# - You can not repackage or sell the codebase
|
# - You can not repackage or sell the codebase
|
||||||
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
||||||
#
|
#
|
||||||
# To enable enterprise-only features, fill your license key in here.
|
# To enable enterprise-only features, as an admin, go to /auth/setup to select your license and follow
|
||||||
|
# instructions. This environment variable is deprecated although still supported for backward compatibility.
|
||||||
# @see https://console.cal.com
|
# @see https://console.cal.com
|
||||||
CALCOM_LICENSE_KEY=
|
CALCOM_LICENSE_KEY=
|
||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
@@ -157,3 +158,9 @@ NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
|
|||||||
# Set this to true in to disable new signups
|
# Set this to true in to disable new signups
|
||||||
# NEXT_PUBLIC_DISABLE_SIGNUP=true
|
# NEXT_PUBLIC_DISABLE_SIGNUP=true
|
||||||
NEXT_PUBLIC_DISABLE_SIGNUP=
|
NEXT_PUBLIC_DISABLE_SIGNUP=
|
||||||
|
|
||||||
|
# Content Security Policy
|
||||||
|
CSP_POLICY=
|
||||||
|
|
||||||
|
# Vercel Edge Config
|
||||||
|
EDGE_CONFIG=
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 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
|
||||||
|
packages/dayjs @zomars
|
||||||
|
packages/emails @zomars
|
||||||
|
packages/embeds @hariombalhara
|
||||||
|
packages/eslint-plugin @zomars
|
||||||
|
packages/features @zomars
|
||||||
|
packages/features/kbar @alishaz-polymath
|
||||||
|
packages/features/ee/impersonation @sean-brydon
|
||||||
|
packages/features/ee/payments @alannnc
|
||||||
|
packages/features/ee/workflows @CarinaWolli
|
||||||
|
packages/features/tips @PeerRich
|
||||||
|
packages/ui @calcom/ui
|
||||||
|
packages/config @calcom/core
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
name: Cache production build binaries
|
||||||
|
description: "Cache or restore if necessary"
|
||||||
|
env:
|
||||||
|
node_version: v16.x
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Cache production build
|
||||||
|
uses: actions/cache@v3
|
||||||
|
env:
|
||||||
|
cache-name: prod-build
|
||||||
|
key-1: ${{ env.node_version }}-${{ hashFiles('yarn.lock') }}
|
||||||
|
key-2: ${{ hashFiles('apps/web/next.config.js') }}
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
apps/web/.next
|
||||||
|
node_modules/.cache/turbo
|
||||||
|
**/.turbo/**
|
||||||
|
**/dist/**
|
||||||
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-
|
||||||
|
${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: Cache database between jobs
|
||||||
|
description: "Cache or restore if necessary"
|
||||||
|
inputs:
|
||||||
|
DATABASE_URL:
|
||||||
|
required: true
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
path: ${{ inputs.path }}
|
||||||
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-
|
||||||
|
- 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,104 @@
|
|||||||
|
name: Production Build
|
||||||
|
description: "Creates a production build, caches it and restores if necessary"
|
||||||
|
inputs:
|
||||||
|
DATABASE_URL:
|
||||||
|
required: true
|
||||||
|
NEXT_PUBLIC_WEBAPP_URL:
|
||||||
|
required: true
|
||||||
|
NEXT_PUBLIC_WEBSITE_URL:
|
||||||
|
required: true
|
||||||
|
NEXTAUTH_SECRET:
|
||||||
|
required: true
|
||||||
|
GOOGLE_API_CREDENTIALS:
|
||||||
|
required: true
|
||||||
|
GOOGLE_LOGIN_ENABLED:
|
||||||
|
required: true
|
||||||
|
CRON_API_KEY:
|
||||||
|
required: false
|
||||||
|
CALENDSO_ENCRYPTION_KEY:
|
||||||
|
required: true
|
||||||
|
DAILY_API_KEY:
|
||||||
|
required: true
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLIC_KEY:
|
||||||
|
required: true
|
||||||
|
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE:
|
||||||
|
required: true
|
||||||
|
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE:
|
||||||
|
required: true
|
||||||
|
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE:
|
||||||
|
required: true
|
||||||
|
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE:
|
||||||
|
required: true
|
||||||
|
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN:
|
||||||
|
required: true
|
||||||
|
STRIPE_PRIVATE_KEY:
|
||||||
|
required: true
|
||||||
|
STRIPE_CLIENT_ID:
|
||||||
|
required: true
|
||||||
|
STRIPE_WEBHOOK_SECRET:
|
||||||
|
required: true
|
||||||
|
STRIPE_PRO_PLAN_PRODUCT_ID:
|
||||||
|
required: true
|
||||||
|
STRIPE_PREMIUM_PLAN_PRODUCT_ID:
|
||||||
|
required: true
|
||||||
|
STRIPE_FREE_PLAN_PRODUCT_ID:
|
||||||
|
required: true
|
||||||
|
PAYMENT_FEE_PERCENTAGE:
|
||||||
|
required: true
|
||||||
|
PAYMENT_FEE_FIXED:
|
||||||
|
required: true
|
||||||
|
SAML_DATABASE_URL:
|
||||||
|
required: true
|
||||||
|
SAML_ADMINS:
|
||||||
|
required: true
|
||||||
|
NEXTAUTH_URL:
|
||||||
|
required: true
|
||||||
|
NEXT_PUBLIC_IS_E2E:
|
||||||
|
required: true
|
||||||
|
TURBO_TOKEN:
|
||||||
|
required: true
|
||||||
|
TURBO_TEAM:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ./.github/actions/yarn-install
|
||||||
|
- uses: ./.github/actions/cache-build
|
||||||
|
id: prod-build-cache
|
||||||
|
- name: Next.js production build
|
||||||
|
if: steps.prod-build-cache.outputs.cache-hit != 'true'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
DATABASE_URL: ${{ inputs.DATABASE_URL }}
|
||||||
|
NEXT_PUBLIC_WEBAPP_URL: ${{ inputs.NEXT_PUBLIC_WEBAPP_URL }}
|
||||||
|
NEXT_PUBLIC_WEBSITE_URL: ${{ inputs.NEXT_PUBLIC_WEBSITE_URL }}
|
||||||
|
NEXTAUTH_SECRET: ${{ inputs.NEXTAUTH_SECRET }}
|
||||||
|
GOOGLE_API_CREDENTIALS: ${{ inputs.GOOGLE_API_CREDENTIALS }}
|
||||||
|
GOOGLE_LOGIN_ENABLED: ${{ inputs.GOOGLE_LOGIN_ENABLED }}
|
||||||
|
CALENDSO_ENCRYPTION_KEY: ${{ inputs.CALENDSO_ENCRYPTION_KEY }}
|
||||||
|
DAILY_API_KEY: ${{ inputs.DAILY_API_KEY }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ inputs.NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||||
|
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ inputs.NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ inputs.NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ inputs.NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE: ${{ inputs.NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN: ${{ inputs.NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN }}
|
||||||
|
STRIPE_PRIVATE_KEY: ${{ inputs.STRIPE_PRIVATE_KEY }}
|
||||||
|
STRIPE_CLIENT_ID: ${{ inputs.STRIPE_CLIENT_ID }}
|
||||||
|
STRIPE_WEBHOOK_SECRET: ${{ inputs.STRIPE_WEBHOOK_SECRET }}
|
||||||
|
STRIPE_PRO_PLAN_PRODUCT_ID: ${{ inputs.STRIPE_PRO_PLAN_PRODUCT_ID }}
|
||||||
|
STRIPE_PREMIUM_PLAN_PRODUCT_ID: ${{ inputs.STRIPE_PREMIUM_PLAN_PRODUCT_ID }}
|
||||||
|
STRIPE_FREE_PLAN_PRODUCT_ID: ${{ inputs.STRIPE_FREE_PLAN_PRODUCT_ID }}
|
||||||
|
PAYMENT_FEE_PERCENTAGE: ${{ inputs.PAYMENT_FEE_PERCENTAGE }}
|
||||||
|
PAYMENT_FEE_FIXED: ${{ inputs.PAYMENT_FEE_FIXED }}
|
||||||
|
SAML_DATABASE_URL: ${{ inputs.SAML_DATABASE_URL }}
|
||||||
|
SAML_ADMINS: ${{ inputs.SAML_ADMINS }}
|
||||||
|
NEXTAUTH_URL: ${{ inputs.NEXTAUTH_URL }}
|
||||||
|
NEXT_PUBLIC_IS_E2E: ${{ inputs.NEXT_PUBLIC_IS_E2E }}
|
||||||
|
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||||
|
run: |
|
||||||
|
yarn db-seed
|
||||||
|
yarn build
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: Yarn install
|
||||||
|
description: "Install all NPM dependencies, caches them and restores if necessary"
|
||||||
|
env:
|
||||||
|
node_version: v16.x
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Use Node ${{ env.node_version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.node_version }}
|
||||||
|
cache: "yarn"
|
||||||
|
- name: node_modules cache
|
||||||
|
id: node-modules-cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
env:
|
||||||
|
cache-name: node-modules-yarn
|
||||||
|
cache-key: ${{ env.node_version }}-${{ hashFiles('yarn.lock', 'packages/prisma/schema.prisma') }}
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
**/node_modules/**
|
||||||
|
packages/prisma/zod
|
||||||
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.cache-key }}
|
||||||
|
restore-keys: ${{ runner.os }}-${{ env.cache-name }}-
|
||||||
|
- name: Yarn install
|
||||||
|
shell: bash
|
||||||
|
if: steps.node-modules-cache.outputs.cache-hit != 'true'
|
||||||
|
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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: Cron - mark stale for inactive issues
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||||
|
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||||
|
schedule:
|
||||||
|
# Runs every day (see https://crontab.guru)
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v7
|
||||||
|
with:
|
||||||
|
days-before-stale: 60
|
||||||
|
include-only-assigned: true
|
||||||
|
days-before-close: -1
|
||||||
|
|
||||||
|
|
||||||
@@ -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,29 +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:
|
env:
|
||||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||||
@@ -65,32 +48,15 @@ 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
|
||||||
|
- uses: ./.github/actions/cache-db
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
DATABASE_URL: ${{ env.DATABASE_URL }}
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +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:
|
env:
|
||||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||||
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
||||||
@@ -61,35 +51,17 @@ 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
|
||||||
|
- uses: ./.github/actions/cache-db
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
DATABASE_URL: ${{ env.DATABASE_URL }}
|
||||||
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
|
||||||
|
|
||||||
- name: Upload embed-core results
|
- name: Upload embed-core results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
|
|||||||
@@ -1,20 +1,10 @@
|
|||||||
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: ubuntu-latest
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||||
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
||||||
@@ -64,34 +54,16 @@ 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
|
||||||
|
- uses: ./.github/actions/cache-db
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
DATABASE_URL: ${{ env.DATABASE_URL }}
|
||||||
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
|
|
||||||
- run: yarn test-e2e
|
- run: yarn test-e2e
|
||||||
|
|
||||||
- name: Upload test results
|
- name: Upload test results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
|
|||||||
@@ -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: buildjet-4vcpu-ubuntu-2204
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
name: "Next.js Bundle Analysis"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Production build
|
||||||
|
uses: ./.github/workflows/production-build.yml
|
||||||
|
secrets: inherit
|
||||||
|
analyze:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
- uses: ./.github/actions/yarn-install
|
||||||
|
- uses: ./.github/actions/cache-build
|
||||||
|
- name: Analyze bundle
|
||||||
|
run: |
|
||||||
|
cd apps/web
|
||||||
|
npx -p nextjs-bundle-analysis report
|
||||||
|
|
||||||
|
- name: Upload bundle
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: bundle
|
||||||
|
path: apps/web/.next/analyze/__bundle_analysis.json
|
||||||
|
|
||||||
|
- name: Download base branch bundle stats
|
||||||
|
uses: dawidd6/action-download-artifact@v2
|
||||||
|
if: success() && github.event.number
|
||||||
|
with:
|
||||||
|
workflow: nextjs-bundle-analysis.yml
|
||||||
|
branch: ${{ github.event.pull_request.base.ref }}
|
||||||
|
path: apps/web/.next/analyze/base
|
||||||
|
|
||||||
|
# And here's the second place - this runs after we have both the current and
|
||||||
|
# base branch bundle stats, and will compare them to determine what changed.
|
||||||
|
# There are two configurable arguments that come from package.json:
|
||||||
|
#
|
||||||
|
# - budget: optional, set a budget (bytes) against which size changes are measured
|
||||||
|
# it's set to 350kb here by default, as informed by the following piece:
|
||||||
|
# https://infrequently.org/2021/03/the-performance-inequality-gap/
|
||||||
|
#
|
||||||
|
# - red-status-percentage: sets the percent size increase where you get a red
|
||||||
|
# status indicator, defaults to 20%
|
||||||
|
#
|
||||||
|
# Either of these arguments can be changed or removed by editing the `nextBundleAnalysis`
|
||||||
|
# entry in your package.json file.
|
||||||
|
- name: Compare with base branch bundle
|
||||||
|
if: success() && github.event.number
|
||||||
|
run: |
|
||||||
|
cd apps/web
|
||||||
|
ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare
|
||||||
|
|
||||||
|
- name: Get comment body
|
||||||
|
id: get-comment-body
|
||||||
|
if: success() && github.event.number
|
||||||
|
run: |
|
||||||
|
cd apps/web
|
||||||
|
body=$(cat .next/analyze/__bundle_analysis_comment.txt)
|
||||||
|
body="${body//'%'/'%25'}"
|
||||||
|
body="${body//$'\n'/'%0A'}"
|
||||||
|
body="${body//$'\r'/'%0D'}"
|
||||||
|
echo ::set-output name=body::$body
|
||||||
|
|
||||||
|
- name: Find Comment
|
||||||
|
uses: peter-evans/find-comment@v1
|
||||||
|
if: success() && github.event.number
|
||||||
|
id: fc
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.number }}
|
||||||
|
body-includes: "<!-- __NEXTJS_BUNDLE -->"
|
||||||
|
|
||||||
|
- name: Create Comment
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||||
|
if: success() && github.event.number && steps.fc.outputs.comment-id == 0
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.number }}
|
||||||
|
body: ${{ steps.get-comment-body.outputs.body }}
|
||||||
|
|
||||||
|
- name: Update Comment
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||||
|
if: success() && github.event.number && steps.fc.outputs.comment-id != 0
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.number }}
|
||||||
|
body: ${{ steps.get-comment-body.outputs.body }}
|
||||||
|
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||||
|
edit-mode: replace
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
name: PR Update
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
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/**'
|
||||||
|
|
||||||
|
setup:
|
||||||
|
name: Yarn install & cache
|
||||||
|
uses: ./.github/workflows/yarn-install.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
type-check:
|
||||||
|
name: Type check
|
||||||
|
needs: setup
|
||||||
|
uses: ./.github/workflows/check-types.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Unit tests
|
||||||
|
needs: setup
|
||||||
|
uses: ./.github/workflows/test.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Linters
|
||||||
|
needs: setup
|
||||||
|
uses: ./.github/workflows/lint.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Production build
|
||||||
|
needs: setup
|
||||||
|
uses: ./.github/workflows/production-build.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e:
|
||||||
|
name: E2E tests
|
||||||
|
needs: [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.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
analyze:
|
||||||
|
needs: build
|
||||||
|
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||||
|
secrets: inherit
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
name: Production Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
env:
|
||||||
|
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||||
|
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
||||||
|
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
|
||||||
|
NEXTAUTH_SECRET: secret
|
||||||
|
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||||
|
GOOGLE_LOGIN_ENABLED: true
|
||||||
|
# CRON_API_KEY: xxx
|
||||||
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
|
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||||
|
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE: ${{ secrets.NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN: 1
|
||||||
|
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||||
|
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||||
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||||
|
STRIPE_PRO_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_PRO_PLAN_PRODUCT_ID }}
|
||||||
|
STRIPE_PREMIUM_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_PREMIUM_PLAN_PRODUCT_ID }}
|
||||||
|
STRIPE_FREE_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_FREE_PLAN_PRODUCT_ID }}
|
||||||
|
PAYMENT_FEE_PERCENTAGE: 0.005
|
||||||
|
PAYMENT_FEE_FIXED: 10
|
||||||
|
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||||
|
SAML_ADMINS: pro@example.com
|
||||||
|
NEXTAUTH_URL: http://localhost:3000/api/auth
|
||||||
|
NEXT_PUBLIC_IS_E2E: 1
|
||||||
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
jobs:
|
||||||
|
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/yarn-install
|
||||||
|
- uses: ./.github/actions/yarn-build
|
||||||
|
with:
|
||||||
|
# GitHub Composite actions cannot read secrets, we have to pass them in here
|
||||||
|
DATABASE_URL: ${{ env.DATABASE_URL }}
|
||||||
|
NEXT_PUBLIC_WEBAPP_URL: ${{ env.NEXT_PUBLIC_WEBAPP_URL }}
|
||||||
|
NEXT_PUBLIC_WEBSITE_URL: ${{ env.NEXT_PUBLIC_WEBSITE_URL }}
|
||||||
|
NEXTAUTH_SECRET: ${{ env.NEXTAUTH_SECRET }}
|
||||||
|
GOOGLE_API_CREDENTIALS: ${{ env.GOOGLE_API_CREDENTIALS }}
|
||||||
|
GOOGLE_LOGIN_ENABLED: ${{ env.GOOGLE_LOGIN_ENABLED }}
|
||||||
|
CALENDSO_ENCRYPTION_KEY: ${{ env.CALENDSO_ENCRYPTION_KEY }}
|
||||||
|
DAILY_API_KEY: ${{ env.DAILY_API_KEY }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||||
|
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ env.NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ env.NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ env.NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE: ${{ env.NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN: ${{ env.NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN }}
|
||||||
|
STRIPE_PRIVATE_KEY: ${{ env.STRIPE_PRIVATE_KEY }}
|
||||||
|
STRIPE_CLIENT_ID: ${{ env.STRIPE_CLIENT_ID }}
|
||||||
|
STRIPE_WEBHOOK_SECRET: ${{ env.STRIPE_WEBHOOK_SECRET }}
|
||||||
|
STRIPE_PRO_PLAN_PRODUCT_ID: ${{ env.STRIPE_PRO_PLAN_PRODUCT_ID }}
|
||||||
|
STRIPE_PREMIUM_PLAN_PRODUCT_ID: ${{ env.STRIPE_PREMIUM_PLAN_PRODUCT_ID }}
|
||||||
|
STRIPE_FREE_PLAN_PRODUCT_ID: ${{ env.STRIPE_FREE_PLAN_PRODUCT_ID }}
|
||||||
|
PAYMENT_FEE_PERCENTAGE: ${{ env.PAYMENT_FEE_PERCENTAGE }}
|
||||||
|
PAYMENT_FEE_FIXED: ${{ env.PAYMENT_FEE_FIXED }}
|
||||||
|
SAML_DATABASE_URL: ${{ env.SAML_DATABASE_URL }}
|
||||||
|
SAML_ADMINS: ${{ env.SAML_ADMINS }}
|
||||||
|
NEXTAUTH_URL: ${{ env.NEXTAUTH_URL }}
|
||||||
|
NEXT_PUBLIC_IS_E2E: ${{ env.NEXT_PUBLIC_IS_E2E }}
|
||||||
|
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||||
|
- uses: ./.github/actions/cache-db
|
||||||
|
with:
|
||||||
|
DATABASE_URL: ${{ env.DATABASE_URL }}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
name: TODO to Issue
|
||||||
|
on:
|
||||||
|
pull_request_target: # So we can test on forks
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
MANUAL_COMMIT_REF:
|
||||||
|
description: "The SHA of the commit to get the diff for"
|
||||||
|
required: true
|
||||||
|
MANUAL_BASE_REF:
|
||||||
|
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
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
jobs:
|
||||||
|
todos:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
fetch-depth: 2
|
||||||
|
- name: "Add TODOs to issues"
|
||||||
|
uses: "alstr/todo-to-issue-action@v4"
|
||||||
|
env:
|
||||||
|
MANUAL_COMMIT_REF: ${{ inputs.MANUAL_COMMIT_REF }}
|
||||||
|
MANUAL_BASE_REF: ${{ inputs.MANUAL_BASE_REF }}
|
||||||
|
with:
|
||||||
|
IDENTIFIERS: '[{"name": "TODO", "labels": ["🙋🏻♂️help wanted"]}, {"name": "FIXME", "labels": ["🐛 bug"]}]'
|
||||||
@@ -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
|
||||||
@@ -77,6 +77,9 @@ lint-results
|
|||||||
#Storybook
|
#Storybook
|
||||||
apps/storybook/build-storybook.log
|
apps/storybook/build-storybook.log
|
||||||
|
|
||||||
|
# Infisical
|
||||||
|
.infisical.json
|
||||||
|
|
||||||
# Snaplet
|
# Snaplet
|
||||||
.snaplet/snapshots
|
.snaplet/snapshots
|
||||||
.snaplet/structure.d.ts
|
.snaplet/structure.d.ts
|
||||||
|
|||||||
@@ -4,6 +4,60 @@ Contributions are what make the open source community such an amazing place to l
|
|||||||
|
|
||||||
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
|
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
|
||||||
|
|
||||||
|
## Priorities
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Type of Issue
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Priority
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Minor improvements, non-core feature requests
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+sort:updated-desc+label:%22Low+priority%22">
|
||||||
|
<img src="https://img.shields.io/badge/-Low%20Priority-green">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Confusing UX (... but working)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+sort:updated-desc+label:%22Medium+priority%22">
|
||||||
|
<img src="https://img.shields.io/badge/-Medium%20Priority-yellow">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Core Features (Booking page, availabilty, timezone calculation)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+sort:updated-desc+label:%22High+priority%22">
|
||||||
|
<img src="https://img.shields.io/badge/-High%20Priority-orange">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Core Bugs (Login, Booking page, Emails are not working)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+sort:updated-desc+label:Urgent">
|
||||||
|
<img src="https://img.shields.io/badge/-Urgent-red">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
|
|
||||||
The development branch is `main`. This is the branch that all pull
|
The development branch is `main`. This is the branch that all pull
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
|
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
|
||||||
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
|
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
|
||||||
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
|
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
|
||||||
|
<a href="https://console.algora.io/org/cal/bounties?status=open"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Fcal%2Fbounties%3Fstatus%3Dopen" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- ABOUT THE PROJECT -->
|
<!-- ABOUT THE PROJECT -->
|
||||||
@@ -76,8 +77,6 @@ That's where Cal.com comes in. Self-hosted or hosted by us. White-label by desig
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### [Product Hunt](https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso)
|
#### [Product Hunt](https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso)
|
||||||
|
|
||||||
<a href="https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=291910&theme=light&period=monthly" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/calendso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=291910&theme=light" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/stories/how-this-open-source-calendly-alternative-rocketed-to-product-of-the-day" target="_blank"><img src="https://cal.com/maker-grant.svg" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=291910&theme=light&period=monthly" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/calendso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=291910&theme=light" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/stories/how-this-open-source-calendly-alternative-rocketed-to-product-of-the-day" target="_blank"><img src="https://cal.com/maker-grant.svg" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
@@ -341,6 +340,17 @@ Please see our [contributing guide](/CONTRIBUTING.md).
|
|||||||
|
|
||||||
We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
|
We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
|
||||||
|
|
||||||
|
<!-- BOUNTIES -->
|
||||||
|
|
||||||
|
### Bounties
|
||||||
|
|
||||||
|
<a href="https://console.algora.io/org/cal/bounties?status=open">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=dark">
|
||||||
|
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
<!-- TRANSLATIONS -->
|
<!-- TRANSLATIONS -->
|
||||||
|
|
||||||
@@ -350,7 +360,9 @@ Don't code but still want to contribute? Join our [slack](https://cal.com/slack)
|
|||||||
|
|
||||||
                                             except for unsafe-inline in style-src . If you have some custom changes in your instance, you might have to make some code change to make your instance CSP compatible. Right now it enables strict CSP only on login page and on other SSR pages it is enabled in Report only mode to detect possible issues. On, SSG pages it is still not supported.
|
||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"keywords": ["react", "typescript", "node", "nextjs", "prisma", "postgres", "trpc"],
|
"keywords": ["react", "typescript", "node", "nextjs", "prisma", "postgres", "trpc"],
|
||||||
"addons": [
|
"addons": [
|
||||||
{
|
{
|
||||||
"plan": "heroku-postgresql:hobby-dev"
|
"plan": "heroku-postgresql:mini"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
@@ -43,15 +43,18 @@
|
|||||||
},
|
},
|
||||||
"SENDGRID_API_KEY": {
|
"SENDGRID_API_KEY": {
|
||||||
"description": "Sendgrid api key. Used for email reminders in workflows",
|
"description": "Sendgrid api key. Used for email reminders in workflows",
|
||||||
"value": ""
|
"value": "",
|
||||||
|
"required": false
|
||||||
},
|
},
|
||||||
"SENDGRID_SYNC_API_KEY": {
|
"SENDGRID_SYNC_API_KEY": {
|
||||||
"description": "Sendgrid internal sync service",
|
"description": "Sendgrid internal sync service",
|
||||||
"value": ""
|
"value": "",
|
||||||
|
"required": false
|
||||||
},
|
},
|
||||||
"SENDGRID_EMAIL": {
|
"SENDGRID_EMAIL": {
|
||||||
"description": "Sendgrid email. Used for email reminders in workflows",
|
"description": "Sendgrid email. Used for email reminders in workflows",
|
||||||
"value": ""
|
"value": "",
|
||||||
|
"required": false
|
||||||
},
|
},
|
||||||
"EMAIL_FROM": {
|
"EMAIL_FROM": {
|
||||||
"description": "Configures the global From: header whilst sending emails",
|
"description": "Configures the global From: header whilst sending emails",
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -25,6 +25,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/config": "*",
|
"@calcom/config": "*",
|
||||||
"eslint": "^8.22.0"
|
"eslint": "^8.34.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
import "nextra-theme-docs/style.css";
|
import "nextra-theme-docs/style.css";
|
||||||
|
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { create } from "@storybook/theming";
|
||||||
|
|
||||||
|
export default create({
|
||||||
|
base: "light",
|
||||||
|
colorPrimary: "#111827",
|
||||||
|
colorSecondary: "#111827",
|
||||||
|
appContentBg: "#F9FAFC",
|
||||||
|
brandTitle: "Cal.com UI",
|
||||||
|
brandUrl: "https://cal.com",
|
||||||
|
brandImage: "https://cal.com/logo.svg",
|
||||||
|
brandTarget: "_self",
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { addons } from "@storybook/addons";
|
||||||
|
|
||||||
|
import yourTheme from "./YourTheme";
|
||||||
|
|
||||||
|
addons.setConfig({
|
||||||
|
theme: yourTheme,
|
||||||
|
});
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.0.0",
|
"@radix-ui/react-dialog": "^1.0.0",
|
||||||
"@radix-ui/react-dropdown-menu": "^1.0.0",
|
"@radix-ui/react-dropdown-menu": "^1.0.0",
|
||||||
"@radix-ui/react-id": "^1.0.0",
|
"@radix-ui/react-id": "^1.0.0",
|
||||||
"@radix-ui/react-popover": "^1.0.0",
|
"@radix-ui/react-popover": "^1.0.2",
|
||||||
"@radix-ui/react-radio-group": "^1.0.0",
|
"@radix-ui/react-radio-group": "^1.0.0",
|
||||||
"@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",
|
||||||
@@ -36,8 +36,8 @@
|
|||||||
"@storybook/manager-webpack5": "^6.5.13",
|
"@storybook/manager-webpack5": "^6.5.13",
|
||||||
"@storybook/react": "^6.5.13",
|
"@storybook/react": "^6.5.13",
|
||||||
"@storybook/testing-library": "^0.0.13",
|
"@storybook/testing-library": "^0.0.13",
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "18.0.9",
|
||||||
"@vitejs/plugin-react": "^2.1.0",
|
"@vitejs/plugin-react": "^2.1.0",
|
||||||
"autoprefixer": "^10.4.12",
|
"autoprefixer": "^10.4.12",
|
||||||
"babel-loader": "^8.2.5",
|
"babel-loader": "^8.2.5",
|
||||||
|
|||||||
@@ -2,4 +2,9 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@import "../../../packages/ui/styles/shared-globals.css"
|
@import "../../../packages/ui/styles/shared-globals.css";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-inter: "Inter var";
|
||||||
|
--font-cal: "Cal Sans";
|
||||||
|
}
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "16.9.1",
|
"@types/node": "16.9.1",
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "18.0.9",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { SwaggerUI } from "swagger-ui-react";
|
import type { SwaggerUI } from "swagger-ui-react";
|
||||||
|
|
||||||
import { SnippedGenerator, requestSnippets } from "@lib/snippets";
|
import { SnippedGenerator, requestSnippets } from "@lib/snippets";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
import { OptionProps } from "react-select";
|
import type { OptionProps } from "react-select";
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ReactNode, useEffect, useRef, useState } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -62,10 +63,9 @@ export default function AppListCard(props: AppListCardProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`p-4 ${highlight ? "bg-yellow-100" : ""}`}>
|
<div className={`${highlight ? "bg-yellow-100" : ""}`}>
|
||||||
<div className="flex gap-x-3">
|
<div className="flex gap-x-3 p-4">
|
||||||
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
|
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
|
||||||
|
|
||||||
<div className="flex grow flex-col gap-y-1 truncate">
|
<div className="flex grow flex-col gap-y-1 truncate">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<h3 className="truncate text-sm font-semibold text-gray-900">{title}</h3>
|
<h3 className="truncate text-sm font-semibold text-gray-900">{title}</h3>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { DestinationCalendar } from "@calcom/prisma/client";
|
import type { DestinationCalendar } from "@calcom/prisma/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
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 { NextRouter, useRouter } from "next/router";
|
import type { NextRouter } from "next/router";
|
||||||
import { createRef, forwardRef, MutableRefObject, RefObject, useRef, useState } from "react";
|
import { useRouter } from "next/router";
|
||||||
import { components, ControlProps } from "react-select";
|
import type { MutableRefObject, RefObject } from "react";
|
||||||
|
import { createRef, forwardRef, useRef, useState } from "react";
|
||||||
|
import type { ControlProps } from "react-select";
|
||||||
|
import { components } from "react-select";
|
||||||
|
|
||||||
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -12,16 +15,15 @@ import {
|
|||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
HorizontalTabs,
|
HorizontalTabs,
|
||||||
InputLeading,
|
|
||||||
Label,
|
Label,
|
||||||
showToast,
|
showToast,
|
||||||
Switch,
|
Switch,
|
||||||
TextArea,
|
TextArea,
|
||||||
TextField,
|
TextField,
|
||||||
|
ColorPicker,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon";
|
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import ColorPicker from "@components/ui/colorpicker";
|
|
||||||
import Select from "@components/ui/form/Select";
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
type EmbedType = "inline" | "floating-popup" | "element-click";
|
type EmbedType = "inline" | "floating-popup" | "element-click";
|
||||||
@@ -676,7 +678,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
return `${router.asPath.split("?")[0]}?${searchParams.toString()}`;
|
return `${router.asPath.split("?")[0]}?${searchParams.toString()}`;
|
||||||
};
|
};
|
||||||
const parsedTabs = tabs.map((t) => ({ ...t, href: s(t.href) }));
|
const parsedTabs = tabs.map((t) => ({ ...t, href: s(t.href) }));
|
||||||
const embedCodeRefs: Record<typeof tabs[0]["name"], RefObject<HTMLTextAreaElement>> = {};
|
const embedCodeRefs: Record<(typeof tabs)[0]["name"], RefObject<HTMLTextAreaElement>> = {};
|
||||||
tabs
|
tabs
|
||||||
.filter((tab) => tab.type === "code")
|
.filter((tab) => tab.type === "code")
|
||||||
.forEach((codeTab) => {
|
.forEach((codeTab) => {
|
||||||
@@ -720,7 +722,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
|
|
||||||
const calLink = decodeURIComponent(embedUrl);
|
const calLink = decodeURIComponent(embedUrl);
|
||||||
|
|
||||||
const addToPalette = (update: typeof previewState["palette"]) => {
|
const addToPalette = (update: (typeof previewState)["palette"]) => {
|
||||||
setPreviewState((previewState) => {
|
setPreviewState((previewState) => {
|
||||||
return {
|
return {
|
||||||
...previewState,
|
...previewState,
|
||||||
@@ -1061,7 +1063,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
defaultValue="#000000"
|
defaultValue="#000000"
|
||||||
onChange={(color) => {
|
onChange={(color) => {
|
||||||
addToPalette({
|
addToPalette({
|
||||||
[palette.name as keyof typeof previewState["palette"]]: color,
|
[palette.name as keyof (typeof previewState)["palette"]]: color,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { Dispatch, useState, useEffect } from "react";
|
import type { Dispatch } from "react";
|
||||||
import { JSONObject } from "superjson/dist/types";
|
import { useState, useEffect } from "react";
|
||||||
|
import type { JSONObject } from "superjson/dist/types";
|
||||||
|
|
||||||
export type Gate = undefined | "rainbow"; // Add more like ` | "geolocation" | "payment"`
|
export type Gate = undefined | "rainbow"; // Add more like ` | "geolocation" | "payment"`
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { FormEvent, useCallback, useEffect, useState } from "react";
|
import type { FormEvent } from "react";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import Cropper from "react-easy-crop";
|
import Cropper from "react-easy-crop";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui";
|
import { Button, Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui";
|
||||||
|
|
||||||
import { Area, getCroppedImg } from "@lib/cropImage";
|
import type { Area } from "@lib/cropImage";
|
||||||
|
import { getCroppedImg } from "@lib/cropImage";
|
||||||
import { useFileReader } from "@lib/hooks/useFileReader";
|
import { useFileReader } from "@lib/hooks/useFileReader";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import Slider from "@components/Slider";
|
import Slider from "@components/Slider";
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { AdminRequired } from "components/ui/AdminRequired";
|
import { AdminRequired } from "components/ui/AdminRequired";
|
||||||
import noop from "lodash/noop";
|
import noop from "lodash/noop";
|
||||||
import Link, { LinkProps } from "next/link";
|
import type { LinkProps } from "next/link";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { FC, Fragment, MouseEventHandler } from "react";
|
import type { FC, MouseEventHandler } from "react";
|
||||||
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
import { SVGComponent } from "@lib/types/SVGComponent";
|
import type { SVGComponent } from "@lib/types/SVGComponent";
|
||||||
|
|
||||||
export interface NavTabProps {
|
export interface NavTabProps {
|
||||||
tabs: {
|
tabs: {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { ErrorBoundary } from "@calcom/ui";
|
import { ErrorBoundary } from "@calcom/ui";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OptionProps } from "react-select";
|
import type { OptionProps } from "react-select";
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import classNames from "@calcom/lib/classNames";
|
|||||||
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } 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 { App as AppType } from "@calcom/types/App";
|
import type { App as AppType } from "@calcom/types/App";
|
||||||
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
||||||
import {
|
import {
|
||||||
FiBookOpen,
|
FiBookOpen,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { DestinationCalendar } from "@calcom/prisma/client";
|
import type { DestinationCalendar } from "@calcom/prisma/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
||||||
import { InstalledAppVariants } from "@calcom/app-store/utils";
|
import type { InstalledAppVariants } from "@calcom/app-store/utils";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
||||||
@@ -44,7 +45,7 @@ export default function InstalledAppsLayout({
|
|||||||
children,
|
children,
|
||||||
...rest
|
...rest
|
||||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||||
const variant: typeof InstalledAppVariants[number] = "payment";
|
const variant: (typeof InstalledAppVariants)[number] = "payment";
|
||||||
|
|
||||||
const query = trpc.viewer.integrations.useQuery({
|
const query = trpc.viewer.integrations.useQuery({
|
||||||
variant,
|
variant,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
|
import { UserPermissionRole } from "@prisma/client";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { ComponentProps, useEffect } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import type Shell from "@calcom/features/shell/Shell";
|
||||||
import { ErrorBoundary } from "@calcom/ui";
|
import { ErrorBoundary } from "@calcom/ui";
|
||||||
|
|
||||||
import { UserPermissionRole } from ".prisma/client";
|
|
||||||
|
|
||||||
export default function AdminLayout({
|
export default function AdminLayout({
|
||||||
children,
|
children,
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { Tooltip } from "@calcom/ui";
|
import { Tooltip } from "@calcom/ui";
|
||||||
import { FiLink } from "@calcom/ui/components/icon";
|
import { FiLink } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { Props } from "./pages/AvailabilityPage";
|
import type { Props } from "./pages/AvailabilityPage";
|
||||||
|
|
||||||
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return locations.length ? (
|
return locations.length ? (
|
||||||
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-2 break-words text-sm text-gray-600">
|
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
|
||||||
{locations.map((location) => {
|
{locations.map((location, index) => {
|
||||||
const eventLocationType = getEventLocationType(location.type);
|
const eventLocationType = getEventLocationType(location.type);
|
||||||
if (!eventLocationType) {
|
if (!eventLocationType) {
|
||||||
// It's possible that the location app got uninstalled
|
// It's possible that the location app got uninstalled
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
|
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||||
{eventLocationType.iconUrl === "/link.svg" ? (
|
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||||
<FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
<FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
||||||
) : (
|
) : (
|
||||||
@@ -26,13 +26,13 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
|||||||
src={eventLocationType.iconUrl}
|
src={eventLocationType.iconUrl}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
||||||
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
|
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
|
||||||
)}
|
)}
|
||||||
alt={`${eventLocationType.label} icon`}
|
alt={`${eventLocationType.label} icon`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
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 { FC, useEffect, useState } from "react";
|
import type { FC } from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import dayjs, { Dayjs } from "@calcom/dayjs";
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { nameOfDay } from "@calcom/lib/weekday";
|
import { nameOfDay } from "@calcom/lib/weekday";
|
||||||
@@ -37,6 +40,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
ethSignature,
|
ethSignature,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { rescheduleUid } = router.query;
|
const { rescheduleUid } = router.query;
|
||||||
@@ -47,122 +51,124 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
setBrand(getComputedStyle(document.documentElement).getPropertyValue("--brand-color").trim());
|
setBrand(getComputedStyle(document.documentElement).getPropertyValue("--brand-color").trim());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!date) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col px-4 text-center sm:mt-0 sm:p-5 md:-mb-5 md:min-w-[200px] lg:min-w-[300px]">
|
<div ref={slotPickerRef}>
|
||||||
<div className="mb-6 flex items-center text-left text-base">
|
{!!date ? (
|
||||||
<div className="mr-4">
|
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col px-4 text-center sm:mt-0 sm:p-5 md:-mb-5 md:min-w-[200px] lg:min-w-[300px]">
|
||||||
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
|
<div className="mb-6 flex items-center text-left text-base">
|
||||||
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
<div className="mr-4">
|
||||||
</span>
|
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
|
||||||
<span className="text-bookinglight font-medium">
|
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||||
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
</span>
|
||||||
</span>
|
<span className="text-bookinglight font-medium">
|
||||||
</div>
|
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
||||||
<div className="ml-auto">
|
</span>
|
||||||
<ToggleGroup
|
</div>
|
||||||
onValueChange={(timeFormat) => onTimeFormatChange(timeFormat === "24")}
|
<div className="ml-auto">
|
||||||
defaultValue={timeFormat === TimeFormat.TWELVE_HOUR ? "12" : "24"}
|
<ToggleGroup
|
||||||
options={[
|
onValueChange={(timeFormat) => onTimeFormatChange(timeFormat === "24")}
|
||||||
{ value: "12", label: t("12_hour_short") },
|
defaultValue={timeFormat === TimeFormat.TWELVE_HOUR ? "12" : "24"}
|
||||||
{ value: "24", label: t("24_hour_short") },
|
options={[
|
||||||
]}
|
{ value: "12", label: t("12_hour_short") },
|
||||||
/>
|
{ value: "24", label: t("24_hour_short") },
|
||||||
</div>
|
]}
|
||||||
</div>
|
/>
|
||||||
<div className="flex-grow overflow-y-auto sm:block md:h-[364px]">
|
</div>
|
||||||
{slots.length > 0 &&
|
|
||||||
slots.map((slot) => {
|
|
||||||
type BookingURL = {
|
|
||||||
pathname: string;
|
|
||||||
query: Record<string, string | number | string[] | undefined>;
|
|
||||||
};
|
|
||||||
const bookingUrl: BookingURL = {
|
|
||||||
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
|
|
||||||
query: {
|
|
||||||
...router.query,
|
|
||||||
date: dayjs.utc(slot.time).tz(timeZone()).format(),
|
|
||||||
type: eventTypeId,
|
|
||||||
slug: eventTypeSlug,
|
|
||||||
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
|
||||||
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
|
||||||
...(ethSignature ? { ethSignature } : {}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (rescheduleUid) {
|
|
||||||
bookingUrl.query.rescheduleUid = rescheduleUid as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If event already has an attendee add booking id
|
|
||||||
if (slot.bookingUid) {
|
|
||||||
bookingUrl.query.bookingUid = slot.bookingUid;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
|
|
||||||
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
|
|
||||||
{/* Current there is no way to disable Next.js Links */}
|
|
||||||
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
|
||||||
<div
|
|
||||||
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 ",
|
|
||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
|
||||||
)}>
|
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
|
||||||
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
href={bookingUrl}
|
|
||||||
prefetch={false}
|
|
||||||
className={classNames(
|
|
||||||
"text-primary-500 hover:border-gray-900 hover:bg-gray-50",
|
|
||||||
"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",
|
|
||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
|
||||||
)}
|
|
||||||
data-testid="time">
|
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
|
||||||
{!!seatsPerTimeSlot && (
|
|
||||||
<p
|
|
||||||
className={`${
|
|
||||||
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
|
|
||||||
? "text-rose-600"
|
|
||||||
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
|
|
||||||
? "text-yellow-500"
|
|
||||||
: "text-emerald-400"
|
|
||||||
} text-sm`}>
|
|
||||||
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
|
|
||||||
{seatsPerTimeSlot} {t("seats_available")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{!isLoading && !slots.length && (
|
|
||||||
<div className="-mt-4 flex h-full w-full flex-col content-center items-center justify-center">
|
|
||||||
<h1 className="my-6 text-xl text-black dark:text-white">{t("all_booked_today")}</h1>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex-grow overflow-y-auto sm:block md:h-[364px]">
|
||||||
|
{slots.length > 0 &&
|
||||||
|
slots.map((slot) => {
|
||||||
|
type BookingURL = {
|
||||||
|
pathname: string;
|
||||||
|
query: Record<string, string | number | string[] | undefined>;
|
||||||
|
};
|
||||||
|
const bookingUrl: BookingURL = {
|
||||||
|
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
|
||||||
|
query: {
|
||||||
|
...router.query,
|
||||||
|
date: dayjs.utc(slot.time).tz(timeZone()).format(),
|
||||||
|
type: eventTypeId,
|
||||||
|
slug: eventTypeSlug,
|
||||||
|
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
||||||
|
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
||||||
|
...(ethSignature ? { ethSignature } : {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
{isLoading && !slots.length && (
|
if (rescheduleUid) {
|
||||||
<>
|
bookingUrl.query.rescheduleUid = rescheduleUid as string;
|
||||||
<SkeletonContainer className="mb-2">
|
}
|
||||||
<SkeletonText className="h-5 w-full" />
|
|
||||||
</SkeletonContainer>
|
// If event already has an attendee add booking id
|
||||||
<SkeletonContainer className="mb-2">
|
if (slot.bookingUid) {
|
||||||
<SkeletonText className="h-5 w-full" />
|
bookingUrl.query.bookingUid = slot.bookingUid;
|
||||||
</SkeletonContainer>
|
}
|
||||||
<SkeletonContainer className="mb-2">
|
|
||||||
<SkeletonText className="h-5 w-full" />
|
return (
|
||||||
</SkeletonContainer>
|
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
|
||||||
</>
|
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
|
||||||
)}
|
{/* Current there is no way to disable Next.js Links */}
|
||||||
</div>
|
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
||||||
|
<div
|
||||||
|
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 ",
|
||||||
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
|
)}>
|
||||||
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
|
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
href={bookingUrl}
|
||||||
|
prefetch={false}
|
||||||
|
className={classNames(
|
||||||
|
"hover:bg-brand hover:border-brand hover:text-brandcontrast dark:hover:text-darkmodebrandcontrast",
|
||||||
|
"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" ? "" : ""
|
||||||
|
)}
|
||||||
|
data-testid="time">
|
||||||
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
|
{!!seatsPerTimeSlot && (
|
||||||
|
<p
|
||||||
|
className={`${
|
||||||
|
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
|
||||||
|
? "text-rose-600"
|
||||||
|
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
|
||||||
|
? "text-yellow-500"
|
||||||
|
: "text-emerald-400"
|
||||||
|
} text-sm`}>
|
||||||
|
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
|
||||||
|
{seatsPerTimeSlot} {t("seats_available")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{!isLoading && !slots.length && (
|
||||||
|
<div className="-mt-4 flex h-full w-full flex-col content-center items-center justify-center">
|
||||||
|
<h1 className="my-6 text-xl text-black dark:text-white">{t("all_booked_today")}</h1>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading && !slots.length && (
|
||||||
|
<>
|
||||||
|
<SkeletonContainer className="mb-2">
|
||||||
|
<SkeletonText className="h-5 w-full" />
|
||||||
|
</SkeletonContainer>
|
||||||
|
<SkeletonContainer className="mb-2">
|
||||||
|
<SkeletonText className="h-5 w-full" />
|
||||||
|
</SkeletonContainer>
|
||||||
|
<SkeletonContainer className="mb-2">
|
||||||
|
<SkeletonText className="h-5 w-full" />
|
||||||
|
</SkeletonContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { SchedulingType } from "@prisma/client";
|
import { SchedulingType } from "@prisma/client";
|
||||||
import { FC, ReactNode, useEffect } from "react";
|
import type { FC, ReactNode } from "react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { classNames } from "@calcom/lib";
|
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, FiInfo } from "@calcom/ui/components/icon";
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ import { BookingStatus } from "@prisma/client";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
|
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { formatTime } from "@calcom/lib/date-fns";
|
import { formatTime } from "@calcom/lib/date-fns";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { RouterInputs, RouterOutputs, trpc } from "@calcom/trpc/react";
|
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import type { ActionType } from "@calcom/ui";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -17,13 +20,11 @@ import {
|
|||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
|
||||||
MeetingTimeInTimezones,
|
MeetingTimeInTimezones,
|
||||||
showToast,
|
showToast,
|
||||||
TextArea,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
ActionType,
|
|
||||||
TableActions,
|
TableActions,
|
||||||
|
TextAreaField,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiCheck, FiClock, FiMapPin, FiRefreshCcw, FiSend, FiSlash, FiX } from "@calcom/ui/components/icon";
|
import { FiCheck, FiClock, FiMapPin, FiRefreshCcw, FiSend, FiSlash, FiX } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
||||||
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data.status === BookingStatus.REJECTED) {
|
if (data?.status === BookingStatus.REJECTED) {
|
||||||
setRejectionDialogIsOpen(false);
|
setRejectionDialogIsOpen(false);
|
||||||
showToast(t("booking_rejection_success"), "success");
|
showToast(t("booking_rejection_success"), "success");
|
||||||
} else {
|
} else {
|
||||||
@@ -245,24 +246,23 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
)}
|
)}
|
||||||
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
|
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
|
||||||
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
|
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
|
||||||
<DialogContent>
|
<DialogContent title={t("rejection_reason_title")} description={t("rejection_reason_description")}>
|
||||||
<DialogHeader title={t("rejection_reason_title")} />
|
<div>
|
||||||
|
<TextAreaField
|
||||||
<p className="-mt-4 text-sm text-gray-500">{t("rejection_reason_description")}</p>
|
name="rejectionReason"
|
||||||
<p className="mt-6 mb-2 text-sm font-bold text-black">
|
label={
|
||||||
{t("rejection_reason")}
|
<>
|
||||||
<span className="font-normal text-gray-500"> (Optional)</span>
|
{t("rejection_reason")}
|
||||||
</p>
|
<span className="font-normal text-gray-500"> (Optional)</span>
|
||||||
<TextArea
|
</>
|
||||||
name={t("rejection_reason")}
|
}
|
||||||
value={rejectionReason}
|
value={rejectionReason}
|
||||||
onChange={(e) => setRejectionReason(e.target.value)}
|
onChange={(e) => setRejectionReason(e.target.value)}
|
||||||
className="mb-5 sm:mb-6"
|
/>
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose />
|
<DialogClose />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={mutation.isLoading}
|
disabled={mutation.isLoading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import type { EventType } from "@prisma/client";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import DatePicker from "@calcom/features/calendars/DatePicker";
|
||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import type { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
|
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
|
const AvailableTimes = dynamic(() => import("@components/booking/AvailableTimes"));
|
||||||
|
|
||||||
|
const getRefetchInterval = (refetchCount: number): number => {
|
||||||
|
const intervals = [3000, 3000, 5000, 10000, 20000, 30000] as const;
|
||||||
|
return intervals[refetchCount] || intervals[intervals.length - 1];
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSlots = ({
|
||||||
|
eventTypeId,
|
||||||
|
eventTypeSlug,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
usernameList,
|
||||||
|
timeZone,
|
||||||
|
duration,
|
||||||
|
enabled = true,
|
||||||
|
}: {
|
||||||
|
eventTypeId: number;
|
||||||
|
eventTypeSlug: string;
|
||||||
|
startTime?: Dayjs;
|
||||||
|
endTime?: Dayjs;
|
||||||
|
usernameList: string[];
|
||||||
|
timeZone?: string;
|
||||||
|
duration?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}) => {
|
||||||
|
const [refetchCount, setRefetchCount] = useState(0);
|
||||||
|
const refetchInterval = getRefetchInterval(refetchCount);
|
||||||
|
const { data, isLoading, isPaused, fetchStatus } = trpc.viewer.public.slots.getSchedule.useQuery(
|
||||||
|
{
|
||||||
|
eventTypeId,
|
||||||
|
eventTypeSlug,
|
||||||
|
usernameList,
|
||||||
|
startTime: startTime?.toISOString() || "",
|
||||||
|
endTime: endTime?.toISOString() || "",
|
||||||
|
timeZone,
|
||||||
|
duration,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!startTime && !!endTime && enabled,
|
||||||
|
refetchInterval,
|
||||||
|
trpc: { context: { skipBatch: true } },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!!data && fetchStatus === "idle") {
|
||||||
|
setRefetchCount(refetchCount + 1);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [fetchStatus, data]);
|
||||||
|
|
||||||
|
// The very first time isPaused is set if auto-fetch is disabled, so isPaused should also be considered a loading state.
|
||||||
|
return { slots: data?.slots || {}, isLoading: isLoading || isPaused };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SlotPicker = ({
|
||||||
|
eventType,
|
||||||
|
timeFormat,
|
||||||
|
onTimeFormatChange,
|
||||||
|
timeZone,
|
||||||
|
recurringEventCount,
|
||||||
|
users,
|
||||||
|
seatsPerTimeSlot,
|
||||||
|
weekStart = 0,
|
||||||
|
ethSignature,
|
||||||
|
}: {
|
||||||
|
eventType: Pick<
|
||||||
|
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
|
||||||
|
"id" | "schedulingType" | "slug" | "length" | "metadata"
|
||||||
|
>;
|
||||||
|
timeFormat: TimeFormat;
|
||||||
|
onTimeFormatChange: (is24Hour: boolean) => void;
|
||||||
|
timeZone?: string;
|
||||||
|
seatsPerTimeSlot?: number;
|
||||||
|
recurringEventCount?: number;
|
||||||
|
users: string[];
|
||||||
|
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
ethSignature?: string;
|
||||||
|
}) => {
|
||||||
|
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
||||||
|
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
||||||
|
let { duration = eventType.length.toString() } = useRouterQuery("duration");
|
||||||
|
const { date, setQuery: setDate } = useRouterQuery("date");
|
||||||
|
const { month, setQuery: setMonth } = useRouterQuery("month");
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
if (!eventType.metadata?.multipleDuration) {
|
||||||
|
duration = eventType.length.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!router.isReady) return;
|
||||||
|
|
||||||
|
// Etc/GMT is not actually a timeZone, so handle this select option explicitly to prevent a hard crash.
|
||||||
|
if (timeZone === "Etc/GMT") {
|
||||||
|
setBrowsingDate(dayjs.utc(month).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0));
|
||||||
|
if (date) {
|
||||||
|
setSelectedDate(dayjs.utc(date));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Set the start of the month without shifting time like startOf() may do.
|
||||||
|
setBrowsingDate(
|
||||||
|
dayjs.tz(month, timeZone).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0)
|
||||||
|
);
|
||||||
|
if (date) {
|
||||||
|
// It's important to set the date immediately to the timeZone, dayjs(date) will convert to browsertime.
|
||||||
|
setSelectedDate(dayjs.tz(date, timeZone));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [router.isReady, month, date, duration, timeZone]);
|
||||||
|
|
||||||
|
const { i18n, isLocaleReady } = useLocale();
|
||||||
|
const { slots: monthSlots, isLoading } = useSlots({
|
||||||
|
eventTypeId: eventType.id,
|
||||||
|
eventTypeSlug: eventType.slug,
|
||||||
|
usernameList: users,
|
||||||
|
startTime:
|
||||||
|
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
|
||||||
|
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
|
||||||
|
: browsingDate?.startOf("month"),
|
||||||
|
endTime: browsingDate?.endOf("month"),
|
||||||
|
timeZone,
|
||||||
|
duration,
|
||||||
|
});
|
||||||
|
const { slots: selectedDateSlots, isLoading: _isLoadingSelectedDateSlots } = useSlots({
|
||||||
|
eventTypeId: eventType.id,
|
||||||
|
eventTypeSlug: eventType.slug,
|
||||||
|
usernameList: users,
|
||||||
|
startTime: selectedDate?.startOf("day"),
|
||||||
|
endTime: selectedDate?.endOf("day"),
|
||||||
|
timeZone,
|
||||||
|
duration,
|
||||||
|
/** Prevent refetching is we already have this data from month slots */
|
||||||
|
enabled: !!selectedDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Hide skeleton if we have the slot loaded in the month query */
|
||||||
|
const isLoadingSelectedDateSlots = (() => {
|
||||||
|
if (!selectedDate) return _isLoadingSelectedDateSlots;
|
||||||
|
if (!!selectedDateSlots[selectedDate.format("YYYY-MM-DD")]) return false;
|
||||||
|
if (!!monthSlots[selectedDate.format("YYYY-MM-DD")]) return false;
|
||||||
|
return false;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DatePicker
|
||||||
|
isLoading={isLoading}
|
||||||
|
className={classNames(
|
||||||
|
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-5 lg:min-w-[455px]",
|
||||||
|
selectedDate ? "sm:dark:border-darkgray-200 border-gray-200 sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
|
||||||
|
)}
|
||||||
|
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
|
||||||
|
locale={isLocaleReady ? i18n.language : "en"}
|
||||||
|
selected={selectedDate}
|
||||||
|
onChange={(newDate) => {
|
||||||
|
setDate(newDate.format("YYYY-MM-DD"));
|
||||||
|
}}
|
||||||
|
onMonthChange={(newMonth) => {
|
||||||
|
setMonth(newMonth.format("YYYY-MM"));
|
||||||
|
}}
|
||||||
|
browsingDate={browsingDate}
|
||||||
|
weekStart={weekStart}
|
||||||
|
/>
|
||||||
|
<AvailableTimes
|
||||||
|
isLoading={isLoadingSelectedDateSlots}
|
||||||
|
slots={
|
||||||
|
selectedDate &&
|
||||||
|
(selectedDateSlots[selectedDate.format("YYYY-MM-DD")] ||
|
||||||
|
monthSlots[selectedDate.format("YYYY-MM-DD")])
|
||||||
|
}
|
||||||
|
date={selectedDate}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
onTimeFormatChange={onTimeFormatChange}
|
||||||
|
eventTypeId={eventType.id}
|
||||||
|
eventTypeSlug={eventType.slug}
|
||||||
|
seatsPerTimeSlot={seatsPerTimeSlot}
|
||||||
|
recurringCount={recurringEventCount}
|
||||||
|
ethSignature={ethSignature}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
import type { FC } from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { ITimezoneOption, TimezoneSelect } from "@calcom/ui";
|
import type { ITimezoneOption } from "@calcom/ui";
|
||||||
|
import { TimezoneSelect } from "@calcom/ui";
|
||||||
|
|
||||||
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
|
|
||||||
import { timeZone } from "../../lib/clock";
|
import { timeZone } from "../../lib/clock";
|
||||||
|
|
||||||
@@ -11,7 +15,8 @@ type Props = {
|
|||||||
|
|
||||||
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||||
const { t } = useLocale();
|
const query = useMeQuery();
|
||||||
|
const userTheme = useTheme(query?.data?.theme).resolvedTheme;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedTimeZone(timeZone());
|
setSelectedTimeZone(timeZone());
|
||||||
@@ -23,18 +28,60 @@ const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
|||||||
}
|
}
|
||||||
}, [selectedTimeZone, onSelectTimeZone]);
|
}, [selectedTimeZone, onSelectTimeZone]);
|
||||||
|
|
||||||
return selectedTimeZone !== "" ? (
|
const customStyles = {
|
||||||
<div className="dark:border-darkgray-300 dark:bg-darkgray-200 rounded-sm border border-gray-200 bg-white px-4 pt-4 pb-3 shadow-sm">
|
option: (_provided: object, state: { isSelected: boolean; isFocused: boolean }) => ({
|
||||||
<div className="mb-4 flex">
|
padding: "10px 12px !important",
|
||||||
<div className="text-sm font-medium text-gray-600 dark:text-white">{t("time_options")}</div>
|
color: state.isSelected || state.isFocused ? "#101010" : "#374151",
|
||||||
</div>
|
backgroundColor: state.isSelected ? "#E5E7EB !important" : "white",
|
||||||
<TimezoneSelect
|
...(userTheme === "dark" && {
|
||||||
id="timeZone"
|
color: state.isSelected || state.isFocused ? "white" : "#80868B",
|
||||||
value={selectedTimeZone}
|
}),
|
||||||
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
}),
|
||||||
className="focus:border-brand mt-1 mb-2 block w-full rounded-md border-gray-300 text-sm focus:ring-black"
|
control: () => ({
|
||||||
/>
|
display: "flex",
|
||||||
</div>
|
cursor: "pointer",
|
||||||
|
backgroundColor: "transparent !important",
|
||||||
|
minWidth: "5rem",
|
||||||
|
height: "24px",
|
||||||
|
minHeight: "24px !important",
|
||||||
|
boxShadow: "none !important",
|
||||||
|
}),
|
||||||
|
singleValue: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
|
||||||
|
...provided,
|
||||||
|
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
|
||||||
|
...(userTheme === "dark" && {
|
||||||
|
color: "#a5a5a5 !important",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
menu: (provided: object) => ({
|
||||||
|
...provided,
|
||||||
|
minWidth: "20rem",
|
||||||
|
margin: "8px 0 0 -20px",
|
||||||
|
}),
|
||||||
|
valueContainer: (provided: object) => ({
|
||||||
|
...provided,
|
||||||
|
padding: "0 0 0 6px",
|
||||||
|
}),
|
||||||
|
dropdownIndicator: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
|
||||||
|
...provided,
|
||||||
|
transform: state.selectProps.menuIsOpen ? "rotate(180deg)" : "",
|
||||||
|
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
|
||||||
|
marginLeft: 4,
|
||||||
|
padding: 0,
|
||||||
|
...(userTheme === "dark" && {
|
||||||
|
color: "#80868B",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return !!selectedTimeZone ? (
|
||||||
|
<TimezoneSelect
|
||||||
|
id="timeZone"
|
||||||
|
value={selectedTimeZone}
|
||||||
|
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
||||||
|
className="flex h-6 text-sm font-medium"
|
||||||
|
styles={customStyles}
|
||||||
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { FiGlobe } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
|
|
||||||
|
import TimeOptions from "@components/booking/TimeOptions";
|
||||||
|
|
||||||
|
export function TimezoneDropdown({
|
||||||
|
onChangeTimeZone,
|
||||||
|
}: {
|
||||||
|
onChangeTimeZone: (newTimeZone: string) => void;
|
||||||
|
timeZone?: string;
|
||||||
|
}) {
|
||||||
|
const handleSelectTimeZone = (newTimeZone: string) => {
|
||||||
|
onChangeTimeZone(newTimeZone);
|
||||||
|
localStorageTimeZone(newTimeZone);
|
||||||
|
};
|
||||||
|
|
||||||
|
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">
|
||||||
|
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
|
||||||
|
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { AvatarGroup, AvatarGroupProps } from "@calcom/ui";
|
import type { AvatarGroupProps } from "@calcom/ui";
|
||||||
|
import { AvatarGroup } from "@calcom/ui";
|
||||||
|
|
||||||
export const UserAvatars = ({
|
export const UserAvatars = ({
|
||||||
profile,
|
profile,
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import dynamic from "next/dynamic";
|
||||||
import { EventType } from "@prisma/client";
|
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useReducer, useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
||||||
import dayjs, { Dayjs } from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
useEmbedStyles,
|
useEmbedStyles,
|
||||||
@@ -17,238 +14,39 @@ import {
|
|||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import DatePicker from "@calcom/features/calendars/DatePicker";
|
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
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 notEmpty from "@calcom/lib/notEmpty";
|
import notEmpty from "@calcom/lib/notEmpty";
|
||||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import { HeadSeo } from "@calcom/ui";
|
import { HeadSeo } from "@calcom/ui";
|
||||||
import { FiChevronDown, FiChevronUp, FiCreditCard, FiGlobe, FiRefreshCcw } from "@calcom/ui/components/icon";
|
import { FiCreditCard, FiRefreshCcw } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
|
||||||
|
|
||||||
import Gates, { Gate, GateState } from "@components/Gates";
|
import type { Gate, GateState } from "@components/Gates";
|
||||||
import AvailableTimes from "@components/booking/AvailableTimes";
|
import Gates from "@components/Gates";
|
||||||
import BookingDescription from "@components/booking/BookingDescription";
|
import BookingDescription from "@components/booking/BookingDescription";
|
||||||
import TimeOptions from "@components/booking/TimeOptions";
|
import { SlotPicker } from "@components/booking/SlotPicker";
|
||||||
import PoweredByCal from "@components/ui/PoweredByCal";
|
|
||||||
|
|
||||||
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
||||||
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
||||||
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
||||||
|
|
||||||
const useSlots = ({
|
const PoweredByCal = dynamic(() => import("@components/ui/PoweredByCal"));
|
||||||
eventTypeId,
|
|
||||||
eventTypeSlug,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
usernameList,
|
|
||||||
timeZone,
|
|
||||||
duration,
|
|
||||||
}: {
|
|
||||||
eventTypeId: number;
|
|
||||||
eventTypeSlug: string;
|
|
||||||
startTime?: Dayjs;
|
|
||||||
endTime?: Dayjs;
|
|
||||||
usernameList: string[];
|
|
||||||
timeZone?: string;
|
|
||||||
duration?: string;
|
|
||||||
}) => {
|
|
||||||
const { data, isLoading, isPaused } = trpc.viewer.public.slots.getSchedule.useQuery(
|
|
||||||
{
|
|
||||||
eventTypeId,
|
|
||||||
eventTypeSlug,
|
|
||||||
usernameList,
|
|
||||||
startTime: startTime?.toISOString() || "",
|
|
||||||
endTime: endTime?.toISOString() || "",
|
|
||||||
timeZone,
|
|
||||||
duration,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!startTime && !!endTime,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const [cachedSlots, setCachedSlots] = useState<NonNullable<typeof data>["slots"]>({});
|
|
||||||
|
|
||||||
useEffect(() => {
|
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
|
||||||
if (data?.slots) {
|
/*const SlotPicker = dynamic(() => import("../SlotPicker").then((mod) => mod.SlotPicker), {
|
||||||
setCachedSlots((c) => ({ ...c, ...data?.slots }));
|
ssr: false,
|
||||||
}
|
loading: () => <div className="mt-8 px-4 pb-4 sm:mt-0 sm:p-4 md:min-w-[300px] md:px-5 lg:min-w-[455px]" />,
|
||||||
}, [data]);
|
});*/
|
||||||
|
const TimezoneDropdown = dynamic(() => import("../TimezoneDropdown").then((mod) => mod.TimezoneDropdown), {
|
||||||
// The very first time isPaused is set if auto-fetch is disabled, so isPaused should also be considered a loading state.
|
ssr: false,
|
||||||
return { slots: cachedSlots, isLoading: isLoading || isPaused };
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const SlotPicker = ({
|
|
||||||
eventType,
|
|
||||||
timeFormat,
|
|
||||||
onTimeFormatChange,
|
|
||||||
timeZone,
|
|
||||||
recurringEventCount,
|
|
||||||
users,
|
|
||||||
seatsPerTimeSlot,
|
|
||||||
weekStart = 0,
|
|
||||||
ethSignature,
|
|
||||||
}: {
|
|
||||||
eventType: Pick<
|
|
||||||
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
|
|
||||||
"id" | "schedulingType" | "slug" | "length" | "metadata"
|
|
||||||
>;
|
|
||||||
timeFormat: TimeFormat;
|
|
||||||
onTimeFormatChange: (is24Hour: boolean) => void;
|
|
||||||
timeZone?: string;
|
|
||||||
seatsPerTimeSlot?: number;
|
|
||||||
recurringEventCount?: number;
|
|
||||||
users: string[];
|
|
||||||
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
||||||
ethSignature?: string;
|
|
||||||
}) => {
|
|
||||||
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
|
||||||
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
|
||||||
let { duration = eventType.length.toString() } = useRouterQuery("duration");
|
|
||||||
const { date, setQuery: setDate } = useRouterQuery("date");
|
|
||||||
const { month, setQuery: setMonth } = useRouterQuery("month");
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
if (!eventType.metadata?.multipleDuration) {
|
|
||||||
duration = eventType.length.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!router.isReady) return;
|
|
||||||
|
|
||||||
// Etc/GMT is not actually a timeZone, so handle this select option explicitly to prevent a hard crash.
|
|
||||||
if (timeZone === "Etc/GMT") {
|
|
||||||
setBrowsingDate(dayjs.utc(month).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0));
|
|
||||||
if (date) {
|
|
||||||
setSelectedDate(dayjs.utc(date));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Set the start of the month without shifting time like startOf() may do.
|
|
||||||
setBrowsingDate(
|
|
||||||
dayjs.tz(month, timeZone).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0)
|
|
||||||
);
|
|
||||||
if (date) {
|
|
||||||
// It's important to set the date immediately to the timeZone, dayjs(date) will convert to browsertime.
|
|
||||||
setSelectedDate(dayjs.tz(date, timeZone));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [router.isReady, month, date, duration, timeZone]);
|
|
||||||
|
|
||||||
const { i18n, isLocaleReady } = useLocale();
|
|
||||||
const { slots: _1 } = useSlots({
|
|
||||||
eventTypeId: eventType.id,
|
|
||||||
eventTypeSlug: eventType.slug,
|
|
||||||
usernameList: users,
|
|
||||||
startTime: selectedDate?.startOf("day"),
|
|
||||||
endTime: selectedDate?.endOf("day"),
|
|
||||||
timeZone,
|
|
||||||
duration,
|
|
||||||
});
|
|
||||||
const { slots: _2, isLoading } = useSlots({
|
|
||||||
eventTypeId: eventType.id,
|
|
||||||
eventTypeSlug: eventType.slug,
|
|
||||||
usernameList: users,
|
|
||||||
startTime:
|
|
||||||
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
|
|
||||||
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
|
|
||||||
: browsingDate?.startOf("month"),
|
|
||||||
endTime: browsingDate?.endOf("month"),
|
|
||||||
timeZone,
|
|
||||||
duration,
|
|
||||||
});
|
|
||||||
|
|
||||||
const slots = useMemo(() => ({ ..._2, ..._1 }), [_1, _2]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DatePicker
|
|
||||||
isLoading={isLoading}
|
|
||||||
className={classNames(
|
|
||||||
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-5 lg:min-w-[455px]",
|
|
||||||
selectedDate ? "sm:dark:border-darkgray-200 border-gray-200 sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
|
|
||||||
)}
|
|
||||||
includedDates={Object.keys(slots).filter((k) => slots[k].length > 0)}
|
|
||||||
locale={isLocaleReady ? i18n.language : "en"}
|
|
||||||
selected={selectedDate}
|
|
||||||
onChange={(newDate) => {
|
|
||||||
setDate(newDate.format("YYYY-MM-DD"));
|
|
||||||
}}
|
|
||||||
onMonthChange={(newMonth) => {
|
|
||||||
setMonth(newMonth.format("YYYY-MM"));
|
|
||||||
}}
|
|
||||||
browsingDate={browsingDate}
|
|
||||||
weekStart={weekStart}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div ref={slotPickerRef}>
|
|
||||||
<AvailableTimes
|
|
||||||
isLoading={isLoading}
|
|
||||||
slots={selectedDate && slots[selectedDate.format("YYYY-MM-DD")]}
|
|
||||||
date={selectedDate}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
onTimeFormatChange={onTimeFormatChange}
|
|
||||||
eventTypeId={eventType.id}
|
|
||||||
eventTypeSlug={eventType.slug}
|
|
||||||
seatsPerTimeSlot={seatsPerTimeSlot}
|
|
||||||
recurringCount={recurringEventCount}
|
|
||||||
ethSignature={ethSignature}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function TimezoneDropdown({
|
|
||||||
onChangeTimeZone,
|
|
||||||
timeZone,
|
|
||||||
}: {
|
|
||||||
onChangeTimeZone: (newTimeZone: string) => void;
|
|
||||||
timeZone?: string;
|
|
||||||
}) {
|
|
||||||
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleSelectTimeZone = (newTimeZone: string) => {
|
|
||||||
onChangeTimeZone(newTimeZone);
|
|
||||||
localStorageTimeZone(newTimeZone);
|
|
||||||
setIsTimeOptionsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
|
||||||
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 !mt-2 inline-block self-start rounded-md px-2 py-2 text-left text-gray-600">
|
|
||||||
<p className="flex items-center text-sm font-medium">
|
|
||||||
<FiGlobe className="min-h-4 min-w-4 ml-[2px] -mt-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]" />
|
|
||||||
{timeZone}
|
|
||||||
{isTimeOptionsOpen ? (
|
|
||||||
<FiChevronUp className="min-h-4 min-w-4 ml-1 inline-block" />
|
|
||||||
) : (
|
|
||||||
<FiChevronDown className="min-h-4 min-w-4 ml-1 inline-block" />
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</Popover.Trigger>
|
|
||||||
<Popover.Portal>
|
|
||||||
<Popover.Content
|
|
||||||
hideWhenDetached
|
|
||||||
align="start"
|
|
||||||
className="animate-fade-in-up absolute left-0 top-2 w-80 max-w-[calc(100vw_-_1.5rem)]">
|
|
||||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
|
||||||
</Popover.Content>
|
|
||||||
</Popover.Portal>
|
|
||||||
</Popover.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dateQuerySchema = z.object({
|
const dateQuerySchema = z.object({
|
||||||
rescheduleUid: z.string().optional().default(""),
|
rescheduleUid: z.string().optional().default(""),
|
||||||
@@ -311,7 +109,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
() => <TimezoneDropdown timeZone={timeZone} onChangeTimeZone={setTimeZone} />,
|
() => <TimezoneDropdown timeZone={timeZone} onChangeTimeZone={setTimeZone} />,
|
||||||
[timeZone]
|
[timeZone]
|
||||||
);
|
);
|
||||||
const stripeAppData = getStripeAppData(eventType);
|
const paymentAppData = getPaymentAppData(eventType);
|
||||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
||||||
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
||||||
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
||||||
@@ -362,10 +160,10 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
isBackgroundTransparent
|
isBackgroundTransparent
|
||||||
? ""
|
? ""
|
||||||
: "dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-white pb-4 md:pb-0",
|
: "dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-white pb-4 md:pb-0",
|
||||||
"border-bookinglightest overflow-hidden md:rounded-md md:border",
|
"border-bookinglightest md:rounded-md md:border",
|
||||||
isEmbed && "mx-auto"
|
isEmbed && "mx-auto"
|
||||||
)}>
|
)}>
|
||||||
<div className="overflow-hidden md:flex">
|
<div className="md:flex">
|
||||||
{showEventTypeDetails && (
|
{showEventTypeDetails && (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -399,14 +197,14 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{stripeAppData.price > 0 && (
|
{paymentAppData.price > 0 && (
|
||||||
<p className="-ml-2 px-2 text-sm font-medium">
|
<p className="-ml-2 px-2 text-sm font-medium">
|
||||||
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<FormattedNumber
|
<FormattedNumber
|
||||||
value={stripeAppData.price / 100.0}
|
value={paymentAppData.price / 100.0}
|
||||||
style="currency"
|
style="currency"
|
||||||
currency={stripeAppData.currency.toUpperCase()}
|
currency={paymentAppData.currency?.toUpperCase()}
|
||||||
/>
|
/>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import { useSession } from "next-auth/react";
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||||
import { useFieldArray, useForm, useWatch, Controller } from "react-hook-form";
|
import { useFieldArray, useForm, useWatch } from "react-hook-form";
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||||
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import {
|
import {
|
||||||
EventLocationType,
|
|
||||||
getEventLocationType,
|
getEventLocationType,
|
||||||
getEventLocationValue,
|
getEventLocationValue,
|
||||||
getHumanReadableLocationValue,
|
getHumanReadableLocationValue,
|
||||||
@@ -21,7 +21,8 @@ import {
|
|||||||
} from "@calcom/app-store/locations";
|
} from "@calcom/app-store/locations";
|
||||||
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
||||||
import { LocationObject, LocationType } from "@calcom/core/location";
|
import type { LocationObject } from "@calcom/core/location";
|
||||||
|
import { LocationType } from "@calcom/core/location";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
@@ -32,7 +33,7 @@ import {
|
|||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
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 { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
@@ -69,12 +70,13 @@ import createBooking from "@lib/mutations/bookings/create-booking";
|
|||||||
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
||||||
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
||||||
|
|
||||||
import Gates, { Gate, GateState } from "@components/Gates";
|
import type { Gate, GateState } from "@components/Gates";
|
||||||
|
import Gates from "@components/Gates";
|
||||||
import BookingDescription from "@components/booking/BookingDescription";
|
import BookingDescription from "@components/booking/BookingDescription";
|
||||||
|
|
||||||
import { BookPageProps } from "../../../pages/[user]/book";
|
import type { BookPageProps } from "../../../pages/[user]/book";
|
||||||
import { HashLinkPageProps } from "../../../pages/d/[link]/book";
|
import type { HashLinkPageProps } from "../../../pages/d/[link]/book";
|
||||||
import { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
import type { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
||||||
|
|
||||||
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
|
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
|
||||||
|
|
||||||
@@ -122,7 +124,7 @@ const BookingPage = ({
|
|||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const stripeAppData = getStripeAppData(eventType);
|
const paymentAppData = getPaymentAppData(eventType);
|
||||||
// Define duration now that we support multiple duration eventTypes
|
// Define duration now that we support multiple duration eventTypes
|
||||||
let duration = eventType.length;
|
let duration = eventType.length;
|
||||||
if (
|
if (
|
||||||
@@ -148,6 +150,7 @@ const BookingPage = ({
|
|||||||
const mutation = useMutation(createBooking, {
|
const mutation = useMutation(createBooking, {
|
||||||
onSuccess: async (responseData) => {
|
onSuccess: async (responseData) => {
|
||||||
const { uid, paymentUid } = responseData;
|
const { uid, paymentUid } = responseData;
|
||||||
|
|
||||||
if (paymentUid) {
|
if (paymentUid) {
|
||||||
return await router.push(
|
return await router.push(
|
||||||
createPaymentLink({
|
createPaymentLink({
|
||||||
@@ -166,7 +169,7 @@ const BookingPage = ({
|
|||||||
isSuccessBookingPage: true,
|
isSuccessBookingPage: true,
|
||||||
email: bookingForm.getValues("email"),
|
email: bookingForm.getValues("email"),
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
formerTime: booking?.startTime.toString(),
|
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -557,14 +560,14 @@ const BookingPage = ({
|
|||||||
{showEventTypeDetails && (
|
{showEventTypeDetails && (
|
||||||
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
|
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
|
||||||
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
||||||
{stripeAppData.price > 0 && (
|
{paymentAppData.price > 0 && (
|
||||||
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
<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]" />
|
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<FormattedNumber
|
<FormattedNumber
|
||||||
value={stripeAppData.price / 100.0}
|
value={paymentAppData.price / 100.0}
|
||||||
style="currency"
|
style="currency"
|
||||||
currency={stripeAppData.currency.toUpperCase()}
|
currency={paymentAppData?.currency?.toUpperCase()}
|
||||||
/>
|
/>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -7,23 +7,24 @@ import { useEffect } from "react";
|
|||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import type { EventLocationType, LocationObject } from "@calcom/app-store/locations";
|
||||||
import {
|
import {
|
||||||
EventLocationType,
|
|
||||||
getEventLocationType,
|
getEventLocationType,
|
||||||
getHumanReadableLocationValue,
|
getHumanReadableLocationValue,
|
||||||
getMessageForOrganizer,
|
getMessageForOrganizer,
|
||||||
LocationObject,
|
|
||||||
LocationType,
|
LocationType,
|
||||||
} from "@calcom/app-store/locations";
|
} from "@calcom/app-store/locations";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
||||||
import { FiMapPin } from "@calcom/ui/components/icon";
|
import { FiMapPin } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
import CheckboxField from "@components/ui/form/CheckboxField";
|
||||||
import LocationSelect, { LocationOption } from "@components/ui/form/LocationSelect";
|
import type { LocationOption } from "@components/ui/form/LocationSelect";
|
||||||
|
import LocationSelect from "@components/ui/form/LocationSelect";
|
||||||
|
|
||||||
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
||||||
|
|
||||||
@@ -187,6 +188,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) =>
|
||||||
@@ -344,7 +346,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>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Dispatch, SetStateAction, useState } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
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";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { FormValues } from "pages/event-types/[type]";
|
import type { FormValues } from "pages/event-types/[type]";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import { components, OptionProps, SingleValueProps } from "react-select";
|
import type { OptionProps, SingleValueProps } from "react-select";
|
||||||
|
import { components } from "react-select";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
@@ -99,100 +100,106 @@ const format = (date: Date, hour12: boolean) =>
|
|||||||
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
|
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
const EventTypeScheduleDetails = () => {
|
||||||
|
const { data: loggedInUser } = useMeQuery();
|
||||||
|
const timeFormat = loggedInUser?.timeFormat;
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const { watch } = useFormContext<FormValues>();
|
const { watch } = useFormContext<FormValues>();
|
||||||
|
|
||||||
const EventTypeSchedule = () => {
|
|
||||||
const me = useMeQuery();
|
|
||||||
const timeFormat = me?.data?.timeFormat;
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<div className="min-w-4 mb-2">
|
|
||||||
<label htmlFor="availability" className="mt-0 flex text-sm font-medium text-gray-700">
|
|
||||||
{t("availability")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<Controller
|
|
||||||
name="schedule"
|
|
||||||
render={({ field }) => (
|
|
||||||
<AvailabilitySelect
|
|
||||||
value={field.value}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
onChange={(selected) => {
|
|
||||||
field.onChange(selected?.value || null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4 rounded border p-4 py-6 pt-2 md:p-8">
|
|
||||||
<ol className="table border-collapse text-sm">
|
|
||||||
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
|
|
||||||
const isAvailable = !!filterDays(index).length;
|
|
||||||
return (
|
|
||||||
<li key={day} className="my-6 flex border-transparent last:mb-2">
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
"w-20 font-medium sm:w-32",
|
|
||||||
!isAvailable && "text-gray-500 opacity-50"
|
|
||||||
)}>
|
|
||||||
{day}
|
|
||||||
</span>
|
|
||||||
{isLoading ? (
|
|
||||||
<SkeletonText className="block h-5 w-60" />
|
|
||||||
) : isAvailable ? (
|
|
||||||
<div className="space-y-3 text-right">
|
|
||||||
{filterDays(index).map((dayRange, i) => (
|
|
||||||
<div key={i} className="flex items-center leading-4">
|
|
||||||
<span className="w-16 sm:w-28 sm:text-left">
|
|
||||||
{format(dayRange.startTime, timeFormat === 12)}
|
|
||||||
</span>
|
|
||||||
<span className="ltr:ml-4 rtl:mr-4">-</span>
|
|
||||||
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ol>
|
|
||||||
<hr />
|
|
||||||
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
|
||||||
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
|
|
||||||
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
|
|
||||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
href={`/availability/${schedule?.schedule.id}`}
|
|
||||||
color="minimal"
|
|
||||||
EndIcon={FiExternalLink}
|
|
||||||
target="_blank"
|
|
||||||
className="justify-center border sm:border-0"
|
|
||||||
rel="noopener noreferrer">
|
|
||||||
{t("edit_availability")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const scheduleId = watch("schedule");
|
const scheduleId = watch("schedule");
|
||||||
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId });
|
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
||||||
|
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
|
||||||
|
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
|
||||||
|
);
|
||||||
|
|
||||||
const filterDays = (dayNum: number) =>
|
const filterDays = (dayNum: number) =>
|
||||||
schedule?.schedule.availability.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
|
schedule?.schedule.availability.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
|
||||||
|
|
||||||
if (!isTeamEvent) {
|
return (
|
||||||
return <EventTypeSchedule />;
|
<div className="space-y-4 rounded border px-6 pb-4">
|
||||||
}
|
<ol className="table border-collapse text-sm">
|
||||||
|
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
|
||||||
|
const isAvailable = !!filterDays(index).length;
|
||||||
|
return (
|
||||||
|
<li key={day} className="my-6 flex border-transparent last:mb-2">
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
"w-20 font-medium sm:w-32",
|
||||||
|
!isAvailable && "text-gray-500 opacity-50"
|
||||||
|
)}>
|
||||||
|
{day}
|
||||||
|
</span>
|
||||||
|
{isLoading ? (
|
||||||
|
<SkeletonText className="block h-5 w-60" />
|
||||||
|
) : isAvailable ? (
|
||||||
|
<div className="space-y-3 text-right">
|
||||||
|
{filterDays(index).map((dayRange, i) => (
|
||||||
|
<div key={i} className="flex items-center leading-4">
|
||||||
|
<span className="w-16 sm:w-28 sm:text-left">
|
||||||
|
{format(dayRange.startTime, timeFormat === 12)}
|
||||||
|
</span>
|
||||||
|
<span className="ltr:ml-4 rtl:mr-4">-</span>
|
||||||
|
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
<hr />
|
||||||
|
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
||||||
|
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
|
||||||
|
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
|
||||||
|
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
href={`/availability/${schedule?.schedule.id}`}
|
||||||
|
disabled={isLoading}
|
||||||
|
color="minimal"
|
||||||
|
EndIcon={FiExternalLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
|
{t("edit_availability")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EventTypeSchedule = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="availability" className="mb-2 block text-sm font-medium leading-none text-gray-700">
|
||||||
|
{t("availability")}
|
||||||
|
</label>
|
||||||
|
<Controller
|
||||||
|
name="schedule"
|
||||||
|
render={({ field }) => (
|
||||||
|
<AvailabilitySelect
|
||||||
|
value={field.value}
|
||||||
|
onBlur={field.onBlur}
|
||||||
|
name={field.name}
|
||||||
|
onChange={(selected) => {
|
||||||
|
field.onChange(selected?.value || null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<EventTypeScheduleDetails />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UseCommonScheduleSettingsToggle = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { resetField, setValue } = useFormContext<FormValues>();
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
name="metadata.config.useHostSchedulesForTeamEvent"
|
name="metadata.config.useHostSchedulesForTeamEvent"
|
||||||
@@ -201,6 +208,11 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
|||||||
checked={!value}
|
checked={!value}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
onChange(!checked);
|
onChange(!checked);
|
||||||
|
if (checked) {
|
||||||
|
resetField("schedule");
|
||||||
|
} else {
|
||||||
|
setValue("schedule", null);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
title={t("choose_common_schedule_team_event")}
|
title={t("choose_common_schedule_team_event")}
|
||||||
description={t("choose_common_schedule_team_event_description")}>
|
description={t("choose_common_schedule_team_event_description")}>
|
||||||
@@ -210,3 +222,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
||||||
|
return isTeamEvent ? <UseCommonScheduleSettingsToggle /> : <EventTypeSchedule />;
|
||||||
|
};
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { EventTypeCustomInputType } from "@prisma/client";
|
import { EventTypeCustomInputType } from "@prisma/client";
|
||||||
import type { CustomInputParsed } from "pages/event-types/[type]";
|
import type { CustomInputParsed } from "pages/event-types/[type]";
|
||||||
import { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { Control, Controller, useFieldArray, useForm, UseFormRegister, useWatch } from "react-hook-form";
|
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 { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, Label, Select, TextField } from "@calcom/ui";
|
import { Button, Label, Select, TextField } from "@calcom/ui";
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
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.getValues("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||||
|
|
||||||
const removeCustom = (index: number) => {
|
const removeCustom = (index: number) => {
|
||||||
formMethods.getValues("customInputs").splice(index, 1);
|
formMethods.getValues("customInputs").splice(index, 1);
|
||||||
@@ -408,7 +408,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<DialogContent
|
<DialogContent
|
||||||
title={t("custom_event_name")}
|
title={t("custom_event_name")}
|
||||||
description={t("custom_event_name_description")}
|
description={t("custom_event_name_description")}
|
||||||
type="creation">
|
type="creation"
|
||||||
|
enableOverflow>
|
||||||
<TextField
|
<TextField
|
||||||
label={t("event_name_in_calendar")}
|
label={t("event_name_in_calendar")}
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
||||||
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
|
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
|
||||||
import { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
||||||
import { EventTypeAppsList } from "@calcom/app-store/utils";
|
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||||
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 { Button, EmptyScreen } from "@calcom/ui";
|
import { Button, EmptyScreen } from "@calcom/ui";
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Controller, useFormContext, UseFormRegisterReturn, useWatch } from "react-hook-form";
|
import type { UseFormRegisterReturn } from "react-hook-form";
|
||||||
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import convertToNewDurationType, { DurationType } from "@calcom/lib/convertToNewDurationType";
|
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
|
||||||
|
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
||||||
import findDurationType from "@calcom/lib/findDurationType";
|
import findDurationType from "@calcom/lib/findDurationType";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { PeriodType } from "@calcom/prisma/client";
|
import type { PeriodType } from "@calcom/prisma/client";
|
||||||
import type { BookingLimit } from "@calcom/types/Calendar";
|
import type { BookingLimit } from "@calcom/types/Calendar";
|
||||||
import { Button, DateRangePicker, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
import { Button, DateRangePicker, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
||||||
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
|
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { EventTypeSetupProps } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||||
|
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
|
|
||||||
import RecurringEventController from "./RecurringEventController";
|
import RecurringEventController from "./RecurringEventController";
|
||||||
|
|
||||||
export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||||
const stripeAppData = getStripeAppData(eventType);
|
const paymentAppData = getPaymentAppData(eventType);
|
||||||
|
|
||||||
const requirePayment = stripeAppData.price > 0;
|
const requirePayment = paymentAppData.price > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import Link from "next/link";
|
|||||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||||
import { MultiValue } from "react-select";
|
import type { MultiValue } from "react-select";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventLocationType, getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } 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 { slugify } from "@calcom/lib/slugify";
|
import { slugify } from "@calcom/lib/slugify";
|
||||||
@@ -17,10 +18,8 @@ import { Button, Label, Select, SettingsToggle, Skeleton, TextField } from "@cal
|
|||||||
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";
|
||||||
import LocationSelect, {
|
import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect";
|
||||||
SingleValueLocationOption,
|
import LocationSelect from "@components/ui/form/LocationSelect";
|
||||||
LocationOption,
|
|
||||||
} from "@components/ui/form/LocationSelect";
|
|
||||||
|
|
||||||
const getLocationFromType = (
|
const getLocationFromType = (
|
||||||
type: EventLocationType["type"],
|
type: EventLocationType["type"],
|
||||||
@@ -34,6 +33,16 @@ const getLocationFromType = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDefaultLocationValue = (options: EventTypeSetupProps["locationOptions"], type: string) => {
|
||||||
|
for (const locationType of options) {
|
||||||
|
for (const location of locationType.options) {
|
||||||
|
if (location.value === type && location.disabled === false) {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const EventSetupTab = (
|
export const EventSetupTab = (
|
||||||
props: Pick<EventTypeSetupProps, "eventType" | "locationOptions" | "team" | "teamMembers">
|
props: Pick<EventTypeSetupProps, "eventType" | "locationOptions" | "team" | "teamMembers">
|
||||||
) => {
|
) => {
|
||||||
@@ -66,10 +75,15 @@ export const EventSetupTab = (
|
|||||||
setShowLocationModal(true);
|
setShowLocationModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeLocation = (selectedLocation: typeof eventType.locations[number]) => {
|
const removeLocation = (selectedLocation: (typeof eventType.locations)[number]) => {
|
||||||
formMethods.setValue(
|
formMethods.setValue(
|
||||||
"locations",
|
"locations",
|
||||||
formMethods.getValues("locations").filter((location) => location.type !== selectedLocation.type),
|
formMethods.getValues("locations").filter((location) => {
|
||||||
|
if (location.type === LocationType.InPerson) {
|
||||||
|
return location.address !== selectedLocation.address;
|
||||||
|
}
|
||||||
|
return location.type !== selectedLocation.type;
|
||||||
|
}),
|
||||||
{ shouldValidate: true }
|
{ shouldValidate: true }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -84,14 +98,14 @@ export const EventSetupTab = (
|
|||||||
...details,
|
...details,
|
||||||
type: newLocationType,
|
type: newLocationType,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
copy[existingIdx] = {
|
|
||||||
...formMethods.getValues("locations")[existingIdx],
|
|
||||||
...details,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
formMethods.setValue("locations", copy);
|
formMethods.setValue("locations", [
|
||||||
|
...copy,
|
||||||
|
...(newLocationType === LocationType.InPerson && editingLocationType === ""
|
||||||
|
? [{ ...details, type: newLocationType }]
|
||||||
|
: []),
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
formMethods.setValue(
|
formMethods.setValue(
|
||||||
"locations",
|
"locations",
|
||||||
@@ -138,13 +152,11 @@ export const EventSetupTab = (
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultValue = locationOptions.find((item) => item.label === "video")?.options;
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{validLocations.length === 0 && (
|
{validLocations.length === 0 && (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<LocationSelect
|
<LocationSelect
|
||||||
defaultValue={defaultValue}
|
|
||||||
placeholder={t("select")}
|
placeholder={t("select")}
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
@@ -175,18 +187,24 @@ export const EventSetupTab = (
|
|||||||
if (!eventLocationType) {
|
if (!eventLocationType) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
// We dont want to translate the string link - it doesnt exist in common.json and it gets prefixed/suffixed with __ or //
|
||||||
|
const eventLabel =
|
||||||
|
eventLocationType.defaultValueVariable === "link"
|
||||||
|
? eventLocationType.label
|
||||||
|
: t(location[eventLocationType.defaultValueVariable] || eventLocationType.label);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={location.type} className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
<li
|
||||||
<div className="flex max-w-full justify-between">
|
key={`${location.type}${index}`}
|
||||||
<div key={index} className="flex flex-grow items-center">
|
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<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">
|
<span className="line-clamp-1 text-sm ltr:ml-1 rtl:mr-1">{eventLabel}</span>
|
||||||
{t(location[eventLocationType.defaultValueVariable] || eventLocationType.label)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<button
|
<button
|
||||||
@@ -230,7 +248,11 @@ export const EventSetupTab = (
|
|||||||
)}
|
)}
|
||||||
{validLocations.length > 0 && validLocations.length !== locationOptions.length && (
|
{validLocations.length > 0 && validLocations.length !== locationOptions.length && (
|
||||||
<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>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { SchedulingType } from "@prisma/client";
|
import { SchedulingType } from "@prisma/client";
|
||||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { Controller, useFormContext, useWatch, Control } from "react-hook-form";
|
import type { Control } from "react-hook-form";
|
||||||
import { Options } from "react-select";
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||||
|
import type { Options } from "react-select";
|
||||||
|
|
||||||
import CheckedTeamSelect, {
|
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||||
CheckedSelectOption,
|
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||||
} from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
|
||||||
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 { Label, Select } from "@calcom/ui";
|
import { Label, Select } from "@calcom/ui";
|
||||||
@@ -25,6 +25,16 @@ const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
|
|||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
|
||||||
|
if (a.label < b.label) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.label > b.label) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
const FixedHosts = ({
|
const FixedHosts = ({
|
||||||
control,
|
control,
|
||||||
labelText,
|
labelText,
|
||||||
@@ -103,7 +113,6 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
|||||||
];
|
];
|
||||||
|
|
||||||
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{team && (
|
{team && (
|
||||||
@@ -128,17 +137,16 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
|||||||
|
|
||||||
{schedulingType === SchedulingType.COLLECTIVE && (
|
{schedulingType === SchedulingType.COLLECTIVE && (
|
||||||
<FixedHosts
|
<FixedHosts
|
||||||
options={teamMembersOptions}
|
options={teamMembersOptions.sort(sortByLabel)}
|
||||||
placeholder={t("add_attendees")}
|
placeholder={t("add_attendees")}
|
||||||
labelText={t("team")}
|
labelText={t("team")}
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{schedulingType === SchedulingType.ROUND_ROBIN && (
|
{schedulingType === SchedulingType.ROUND_ROBIN && (
|
||||||
<>
|
<>
|
||||||
<FixedHosts
|
<FixedHosts
|
||||||
options={teamMembersOptions}
|
options={teamMembersOptions.sort(sortByLabel)}
|
||||||
isOptionDisabled={(option) =>
|
isOptionDisabled={(option) =>
|
||||||
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
|
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
|
||||||
}
|
}
|
||||||
@@ -173,7 +181,7 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
|||||||
)
|
)
|
||||||
.filter(Boolean)}
|
.filter(Boolean)}
|
||||||
controlShouldRenderValue={false}
|
controlShouldRenderValue={false}
|
||||||
options={teamMembers.map(mapUserToValue)}
|
options={teamMembersOptions.sort(sortByLabel)}
|
||||||
isOptionDisabled={(option) =>
|
isOptionDisabled={(option) =>
|
||||||
!!formMethods
|
!!formMethods
|
||||||
.getValues("hostsFixed")
|
.getValues("hostsFixed")
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { EventTypeSetupProps } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TbWebhook } from "react-icons/tb";
|
import { TbWebhook } from "react-icons/tb";
|
||||||
|
|
||||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||||
import { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||||
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||||
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 { Webhook } from "@calcom/prisma/client";
|
import type { Webhook } from "@calcom/prisma/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
import { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
||||||
import { FiPlus } from "@calcom/ui/components/icon";
|
import { FiPlus } from "@calcom/ui/components/icon";
|
||||||
@@ -100,7 +100,7 @@ export const EventTeamWebhooksTab = ({
|
|||||||
<>
|
<>
|
||||||
{webhooks.length ? (
|
{webhooks.length ? (
|
||||||
<>
|
<>
|
||||||
<div className="mt-4 mb-8 rounded-md border">
|
<div className="mb-8 rounded-md border">
|
||||||
{webhooks.map((webhook, index) => {
|
{webhooks.map((webhook, index) => {
|
||||||
return (
|
return (
|
||||||
<WebhookListItem
|
<WebhookListItem
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { TFunction } from "next-i18next";
|
import type { TFunction } from "next-i18next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useMemo, useState, Suspense } from "react";
|
import { useMemo, useState, Suspense } from "react";
|
||||||
import { UseFormReturn } from "react-hook-form";
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
import { TbWebhook } from "react-icons/tb";
|
import { TbWebhook } from "react-icons/tb";
|
||||||
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
@@ -286,6 +286,7 @@ function EventTypeSingleLayout({
|
|||||||
<DropdownMenuItem className="focus:ring-gray-100">
|
<DropdownMenuItem className="focus:ring-gray-100">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
type="button"
|
type="button"
|
||||||
|
color="destructive"
|
||||||
StartIcon={FiTrash}
|
StartIcon={FiTrash}
|
||||||
disabled={!hasPermsToDelete}
|
disabled={!hasPermsToDelete}
|
||||||
onClick={() => setDeleteDialogOpen(true)}>
|
onClick={() => setDeleteDialogOpen(true)}>
|
||||||
@@ -338,7 +339,7 @@ function EventTypeSingleLayout({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
|
"mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
|
||||||
disableBorder ? "border-0 xl:-mt-4 " : "p-2 md:border md:p-6"
|
disableBorder ? "border-0 " : "p-2 md:border md:p-6"
|
||||||
)}>
|
)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
import { UnitTypeLongPlural } from "dayjs";
|
import type { UnitTypeLongPlural } from "dayjs";
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import type { FormValues } from "pages/event-types/[type]";
|
import type { FormValues } from "pages/event-types/[type]";
|
||||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import z from "zod";
|
import type z from "zod";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Alert, Input, Label, SettingsToggle } from "@calcom/ui";
|
import { Alert, Input, Label, SettingsToggle } from "@calcom/ui";
|
||||||
|
|
||||||
type RequiresConfirmationControllerProps = {
|
type RequiresConfirmationControllerProps = {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterInputs, trpc } from "@calcom/trpc/react";
|
import type { RouterInputs } from "@calcom/trpc/react";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import { useForm } from "react-hook-form";
|
|||||||
import { Schedule } from "@calcom/features/schedules";
|
import { Schedule } from "@calcom/features/schedules";
|
||||||
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
|
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc, TRPCClientErrorLike } from "@calcom/trpc/react";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/react";
|
||||||
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Form } from "@calcom/ui";
|
import { Button, Form } from "@calcom/ui";
|
||||||
|
|
||||||
interface ISetupAvailabilityProps {
|
interface ISetupAvailabilityProps {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
import { ArrowRightIcon } from "@heroicons/react/solid";
|
||||||
import MarkdownIt from "markdown-it";
|
import MarkdownIt from "markdown-it";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { FormEvent, useRef, useState } from "react";
|
import type { FormEvent } 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 { 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";
|
||||||
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||||
@@ -34,6 +36,7 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const createEventType = trpc.viewer.eventTypes.create.useMutation();
|
const createEventType = trpc.viewer.eventTypes.create.useMutation();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||||
onSuccess: async (_data, context) => {
|
onSuccess: async (_data, context) => {
|
||||||
@@ -64,6 +67,8 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
const onSubmit = handleSubmit((data: { bio: string }) => {
|
const onSubmit = handleSubmit((data: { bio: string }) => {
|
||||||
const { bio } = data;
|
const { bio } = data;
|
||||||
|
|
||||||
|
telemetry.event(telemetryEventTypes.onboardingFinished);
|
||||||
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
bio,
|
bio,
|
||||||
completedOnboarding: true,
|
completedOnboarding: true,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
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 { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, TimezoneSelect } from "@calcom/ui";
|
import { Button, TimezoneSelect } from "@calcom/ui";
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
const { user, nextStep } = props;
|
const { user, nextStep } = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
||||||
|
const telemetry = useTelemetry();
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -31,6 +32,11 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
},
|
},
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
telemetry.event(telemetryEventTypes.onboardingStarted);
|
||||||
|
}, [telemetry]);
|
||||||
|
|
||||||
const defaultOptions = { required: true, maxLength: 255 };
|
const defaultOptions = { required: true, maxLength: 255 };
|
||||||
|
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { Badge } from "@calcom/ui";
|
import { Badge } from "@calcom/ui";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { SyntheticEvent, useState } from "react";
|
import type { SyntheticEvent } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { BaseSyntheticEvent, useState } from "react";
|
import type { BaseSyntheticEvent } from "react";
|
||||||
|
import React, { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Badge, Button } from "@calcom/ui";
|
import { Badge, Button } from "@calcom/ui";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import DisableTwoFactorModal from "./DisableTwoFactorModal";
|
import DisableTwoFactorModal from "./DisableTwoFactorModal";
|
||||||
import EnableTwoFactorModal from "./EnableTwoFactorModal";
|
import EnableTwoFactorModal from "./EnableTwoFactorModal";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { BaseSyntheticEvent, useState } from "react";
|
import type { BaseSyntheticEvent } from "react";
|
||||||
|
import React, { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
|
|||||||
+30
-10
@@ -1,17 +1,39 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import React from "react";
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
|
||||||
import { isPasswordValid } from "@calcom/lib/auth";
|
import { isPasswordValid } from "@calcom/lib/auth";
|
||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { EmailField, Label, PasswordField, TextField } from "@calcom/ui";
|
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
|
||||||
|
import { FiUserCheck } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
|
||||||
const router = useRouter();
|
const { t } = useLocale();
|
||||||
|
if (props.userCount > 0)
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
id="wizard-step-1"
|
||||||
|
name="wizard-step-1"
|
||||||
|
className="space-y-4"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
props.onSuccess();
|
||||||
|
}}>
|
||||||
|
<EmptyScreen
|
||||||
|
Icon={FiUserCheck}
|
||||||
|
headline={t("admin_user_created")}
|
||||||
|
description={t("admin_user_created_description")}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
return <AdminUser {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AdminUser = (props: { onSubmit: () => void; onError: () => void; onSuccess: () => void }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
@@ -45,11 +67,11 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onError = () => {
|
const onError = () => {
|
||||||
props.setIsLoading(false);
|
props.onError();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = formMethods.handleSubmit(async (data: z.infer<typeof formSchema>) => {
|
const onSubmit = formMethods.handleSubmit(async (data: z.infer<typeof formSchema>) => {
|
||||||
props.setIsLoading(true);
|
props.onSubmit();
|
||||||
const response = await fetch("/api/auth/setup", {
|
const response = await fetch("/api/auth/setup", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -69,9 +91,9 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||||||
email: data.email_address.toLowerCase(),
|
email: data.email_address.toLowerCase(),
|
||||||
password: data.password,
|
password: data.password,
|
||||||
});
|
});
|
||||||
router.replace(`/auth/setup?step=2&category=calendar`);
|
props.onSuccess();
|
||||||
} else {
|
} else {
|
||||||
router.replace("/auth/setup");
|
props.onError();
|
||||||
}
|
}
|
||||||
}, onError);
|
}, onError);
|
||||||
|
|
||||||
@@ -186,5 +208,3 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||||||
</FormProvider>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SetupFormStep1;
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
|
const ENTERPRISE_BOOKING_FEE = "$99"; // TODO: get this from a new API endpoint
|
||||||
|
|
||||||
|
const ChooseLicense = (
|
||||||
|
props: {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onSubmit: (value: string) => void;
|
||||||
|
} & Omit<JSX.IntrinsicElements["form"], "onSubmit" | "onChange">
|
||||||
|
) => {
|
||||||
|
const { value: initialValue = "FREE", onChange, onSubmit, ...rest } = props;
|
||||||
|
const [value, setValue] = useState(initialValue);
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
{...rest}
|
||||||
|
className="space-y-4"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSubmit(value);
|
||||||
|
}}>
|
||||||
|
<RadioGroup.Root
|
||||||
|
defaultValue={initialValue}
|
||||||
|
value={value}
|
||||||
|
aria-label={t("choose_a_license")}
|
||||||
|
className="grid grid-rows-2 gap-4 md:grid-cols-2 md:grid-rows-1"
|
||||||
|
onValueChange={(value) => {
|
||||||
|
onChange(value);
|
||||||
|
setValue(value);
|
||||||
|
}}>
|
||||||
|
<RadioGroup.Item value="FREE">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black",
|
||||||
|
value === "FREE" && "ring-2 ring-black"
|
||||||
|
)}>
|
||||||
|
<h2 className="font-cal text-xl text-black">{t("agplv3_license")}</h2>
|
||||||
|
<p className="font-medium text-green-800">{t("free_license_fee")}</p>
|
||||||
|
<p className="text-gray-500">{t("forever_open_and_free")}</p>
|
||||||
|
<ul className="ml-4 list-disc text-left text-xs text-gray-500">
|
||||||
|
<li>{t("required_to_keep_your_code_open_source")}</li>
|
||||||
|
<li>{t("cannot_repackage_and_resell")}</li>
|
||||||
|
<li>{t("no_enterprise_features")}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
<RadioGroup.Item value="EE">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black",
|
||||||
|
value === "EE" && "ring-2 ring-black"
|
||||||
|
)}>
|
||||||
|
<h2 className="font-cal text-xl text-black">{t("ee_enterprise_license")}</h2>
|
||||||
|
<p className="font-medium text-green-800">
|
||||||
|
{t("enterprise_booking_fee", { enterprise_booking_fee: ENTERPRISE_BOOKING_FEE })}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500">{t("enterprise_license_includes")}</p>
|
||||||
|
<ul className="ml-4 list-disc text-left text-xs text-gray-500">
|
||||||
|
<li>{t("no_need_to_keep_your_code_open_source")}</li>
|
||||||
|
<li>{t("repackage_rebrand_resell")}</li>
|
||||||
|
<li>{t("a_vast_suite_of_enterprise_features")}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
</RadioGroup.Root>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChooseLicense;
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { noop } from "lodash";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { Controller, FormProvider, useForm, useFormState } from "react-hook-form";
|
||||||
|
import * as z from "zod";
|
||||||
|
|
||||||
|
import { classNames } from "@calcom/lib";
|
||||||
|
import { CONSOLE_URL } from "@calcom/lib/constants";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { Button, TextField } from "@calcom/ui";
|
||||||
|
import { FiCheck, FiExternalLink, FiLoader } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
type EnterpriseLicenseFormValues = {
|
||||||
|
licenseKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeSchemaLicenseKey = (args: { callback: (valid: boolean) => void; onSuccessValidate: () => void }) =>
|
||||||
|
z.object({
|
||||||
|
licenseKey: z
|
||||||
|
.string()
|
||||||
|
.uuid({
|
||||||
|
message: "License key must follow UUID format: 8-4-4-4-12",
|
||||||
|
})
|
||||||
|
.superRefine(async (data, ctx) => {
|
||||||
|
const parse = z.string().uuid().safeParse(data);
|
||||||
|
if (parse.success) {
|
||||||
|
args.callback(true);
|
||||||
|
const response = await fetch(`${CONSOLE_URL}/api/license?key=${data}`);
|
||||||
|
args.callback(false);
|
||||||
|
const json = await response.json();
|
||||||
|
if (!json.valid) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `License key ${json.message.toLowerCase()}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
args.onSuccessValidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const EnterpriseLicense = (
|
||||||
|
props: {
|
||||||
|
licenseKey?: string;
|
||||||
|
initialValue?: Partial<EnterpriseLicenseFormValues>;
|
||||||
|
onSuccessValidate: () => void;
|
||||||
|
onSubmit: (value: EnterpriseLicenseFormValues) => void;
|
||||||
|
onSuccess?: (
|
||||||
|
data: RouterOutputs["viewer"]["deploymentSetup"]["update"],
|
||||||
|
variables: RouterInputs["viewer"]["deploymentSetup"]["update"]
|
||||||
|
) => void;
|
||||||
|
} & Omit<JSX.IntrinsicElements["form"], "onSubmit">
|
||||||
|
) => {
|
||||||
|
const { onSubmit, onSuccess = noop, onSuccessValidate = noop, ...rest } = props;
|
||||||
|
const { t } = useLocale();
|
||||||
|
const [checkLicenseLoading, setCheckLicenseLoading] = useState(false);
|
||||||
|
const mutation = trpc.viewer.deploymentSetup.update.useMutation({
|
||||||
|
onSuccess,
|
||||||
|
});
|
||||||
|
|
||||||
|
const schemaLicenseKey = useCallback(
|
||||||
|
() =>
|
||||||
|
makeSchemaLicenseKey({
|
||||||
|
callback: setCheckLicenseLoading,
|
||||||
|
onSuccessValidate,
|
||||||
|
}),
|
||||||
|
[setCheckLicenseLoading, onSuccessValidate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formMethods = useForm<EnterpriseLicenseFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
licenseKey: props.licenseKey || "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(schemaLicenseKey()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = formMethods.handleSubmit((values) => {
|
||||||
|
onSubmit(values);
|
||||||
|
setCheckLicenseLoading(false);
|
||||||
|
mutation.mutate(values);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isDirty, errors } = useFormState(formMethods);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...formMethods}>
|
||||||
|
<form {...rest} className="space-y-4 rounded-md bg-white px-8 py-10" onSubmit={handleSubmit}>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="w-full justify-center text-lg"
|
||||||
|
EndIcon={FiExternalLink}
|
||||||
|
href="https://console.cal.com"
|
||||||
|
target="_blank">
|
||||||
|
{t("purchase_license")}
|
||||||
|
</Button>
|
||||||
|
<div className="relative flex justify-center">
|
||||||
|
<hr className="my-8 w-full border-[1.5px] border-gray-200" />
|
||||||
|
<span className="absolute mt-[22px] bg-white px-3.5 text-sm">OR</span>
|
||||||
|
</div>
|
||||||
|
{t("already_have_key")}
|
||||||
|
<Controller
|
||||||
|
name="licenseKey"
|
||||||
|
control={formMethods.control}
|
||||||
|
render={({ field: { onBlur, onChange, value } }) => (
|
||||||
|
<TextField
|
||||||
|
{...formMethods.register("licenseKey")}
|
||||||
|
className={classNames(
|
||||||
|
"mb-0 group-hover:border-gray-400",
|
||||||
|
(checkLicenseLoading || (errors.licenseKey === undefined && isDirty)) && "border-r-0"
|
||||||
|
)}
|
||||||
|
placeholder="xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx"
|
||||||
|
labelSrOnly={true}
|
||||||
|
value={value}
|
||||||
|
addOnFilled={false}
|
||||||
|
addOnClassname={classNames(
|
||||||
|
"hover:border-gray-300",
|
||||||
|
errors.licenseKey === undefined && isDirty && "group-hover:border-gray-400"
|
||||||
|
)}
|
||||||
|
addOnSuffix={
|
||||||
|
checkLicenseLoading ? (
|
||||||
|
<FiLoader className="h-5 w-5 animate-spin" />
|
||||||
|
) : errors.licenseKey === undefined && isDirty ? (
|
||||||
|
<FiCheck className="h-5 w-5 text-green-700" />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
color={errors.licenseKey ? "warn" : ""}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onChange={async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
formMethods.setValue("licenseKey", e.target.value);
|
||||||
|
await formMethods.trigger("licenseKey");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EnterpriseLicense;
|
||||||
+11
-5
@@ -1,20 +1,26 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { FiCheck } from "@calcom/ui/components/icon";
|
import { FiCheck } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const StepDone = () => {
|
const StepDone = (props: {
|
||||||
|
currentStep: number;
|
||||||
|
nextStepPath: string;
|
||||||
|
setIsLoading: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
id="wizard-step-1"
|
id={`wizard-step-${props.currentStep}`}
|
||||||
name="wizard-step-1"
|
name={`wizard-step-${props.currentStep}`}
|
||||||
className="space-y-4"
|
className="flex justify-center space-y-4"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
|
props.setIsLoading(true);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
router.replace(`/auth/setup?step=2&category=calendar`);
|
router.replace(props.nextStepPath);
|
||||||
}}>
|
}}>
|
||||||
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
||||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import MarkdownIt from "markdown-it";
|
import MarkdownIt from "markdown-it";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { 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 { Avatar } from "@calcom/ui";
|
import { Avatar } from "@calcom/ui";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||||
|
|
||||||
type TeamType = TeamPageProps["team"];
|
type TeamType = TeamPageProps["team"];
|
||||||
@@ -20,7 +19,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 w-[90%] 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,5 +1,6 @@
|
|||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { FC, Fragment } from "react";
|
import type { FC } from "react";
|
||||||
|
import { Fragment } from "react";
|
||||||
|
|
||||||
type AdminRequiredProps = {
|
type AdminRequiredProps = {
|
||||||
as?: keyof JSX.IntrinsicElements;
|
as?: keyof JSX.IntrinsicElements;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ControllerRenderProps } from "react-hook-form";
|
import type { ControllerRenderProps } from "react-hook-form";
|
||||||
|
|
||||||
import { FiEdit2 } from "@calcom/ui/components/icon";
|
import { FiEdit2 } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ const EditableHeading = function EditableHeading({
|
|||||||
<label className="min-w-8 relative inline-block">
|
<label className="min-w-8 relative inline-block">
|
||||||
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
||||||
{!isEditing && isReady && (
|
{!isEditing && isReady && (
|
||||||
<FiEdit2 className="ml-1 inline h-4 w-4 align-top text-gray-700 group-hover:text-gray-500" />
|
<FiEdit2 className=" ml-1 -mt-px inline h-3 w-3 text-gray-500 group-hover:text-gray-500" />
|
||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
{...passThroughProps}
|
{...passThroughProps}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { SVGComponent } from "@lib/types/SVGComponent";
|
import type { SVGComponent } from "@lib/types/SVGComponent";
|
||||||
|
|
||||||
interface LinkIconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
interface LinkIconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
Icon: SVGComponent;
|
Icon: SVGComponent;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React, { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import { Dialog, DialogContent } from "@calcom/ui";
|
import { Dialog, DialogContent } from "@calcom/ui";
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
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";
|
||||||
import { RefCallback, useEffect, useMemo, useState } from "react";
|
import type { RefCallback } from "react";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
|
import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
|
||||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { User } from "@calcom/prisma/client";
|
import type { User } from "@calcom/prisma/client";
|
||||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import type { RouterOutputs } 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, StarIconSolid } from "@calcom/ui/components/icon";
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { debounce, noop } from "lodash";
|
import { debounce, noop } from "lodash";
|
||||||
import { RefCallback, useEffect, useMemo, useState } from "react";
|
import type { RefCallback } from "react";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, TextField } from "@calcom/ui";
|
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, TextField } from "@calcom/ui";
|
||||||
import { FiCheck, FiEdit2 } from "@calcom/ui/components/icon";
|
import { FiCheck, FiEdit2 } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||||
import { User } from "@calcom/prisma/client";
|
import type { User } from "@calcom/prisma/client";
|
||||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
|
|
||||||
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
import { PremiumTextfield } from "./PremiumTextfield";
|
import { PremiumTextfield } from "./PremiumTextfield";
|
||||||
import { UsernameTextfield } from "./UsernameTextfield";
|
import { UsernameTextfield } from "./UsernameTextfield";
|
||||||
@@ -21,7 +24,13 @@ export const UsernameAvailabilityField = ({
|
|||||||
onErrorMutation,
|
onErrorMutation,
|
||||||
user,
|
user,
|
||||||
}: UsernameAvailabilityFieldProps) => {
|
}: UsernameAvailabilityFieldProps) => {
|
||||||
const [currentUsername, setCurrentUsername] = useState(user.username ?? "");
|
const router = useRouter();
|
||||||
|
const [currentUsernameState, setCurrentUsernameState] = useState(user.username || "");
|
||||||
|
const { username: usernameFromQuery, setQuery: setUsernameFromQuery } = useRouterQuery("username");
|
||||||
|
const { username: currentUsername, setQuery: setCurrentUsername } =
|
||||||
|
router.query["username"] && user.username === null
|
||||||
|
? { username: usernameFromQuery, setQuery: setUsernameFromQuery }
|
||||||
|
: { username: currentUsernameState || "", setQuery: setCurrentUsernameState };
|
||||||
const formMethods = useForm({
|
const formMethods = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: currentUsername,
|
username: currentUsername,
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
import { useCallback, useRef, useState } from "react";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { HexColorInput, HexColorPicker } from "react-colorful";
|
|
||||||
|
|
||||||
import { isValidHexCode, fallBackHex } from "@calcom/lib/CustomBranding";
|
|
||||||
import { Swatch } from "@calcom/ui";
|
|
||||||
|
|
||||||
type Handler = (event: MouseEvent | Event) => void;
|
|
||||||
function useEventListener<
|
|
||||||
KW extends keyof WindowEventMap,
|
|
||||||
KH extends keyof HTMLElementEventMap,
|
|
||||||
T extends HTMLElement | void = void
|
|
||||||
>(
|
|
||||||
eventName: KW | KH,
|
|
||||||
handler: (event: WindowEventMap[KW] | HTMLElementEventMap[KH] | Event) => void,
|
|
||||||
element?: React.RefObject<T>
|
|
||||||
) {
|
|
||||||
// Create a ref that stores handler
|
|
||||||
const savedHandler = useRef<typeof handler>();
|
|
||||||
useEffect(() => {
|
|
||||||
// Define the listening target
|
|
||||||
const targetElement: T | Window = element?.current || window;
|
|
||||||
if (!(targetElement && targetElement.addEventListener)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Update saved handler if necessary
|
|
||||||
if (savedHandler.current !== handler) {
|
|
||||||
savedHandler.current = handler;
|
|
||||||
}
|
|
||||||
// Create event listener that calls handler function stored in ref
|
|
||||||
const eventListener: typeof handler = (event) => {
|
|
||||||
// eslint-disable-next-line no-extra-boolean-cast
|
|
||||||
if (!!savedHandler?.current) {
|
|
||||||
savedHandler.current(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
targetElement.addEventListener(eventName, eventListener);
|
|
||||||
// Remove event listener on cleanup
|
|
||||||
return () => {
|
|
||||||
targetElement.removeEventListener(eventName, eventListener);
|
|
||||||
};
|
|
||||||
}, [eventName, element, handler]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useOnClickOutside<T extends HTMLElement = HTMLElement>(
|
|
||||||
ref: React.RefObject<T>,
|
|
||||||
handler: Handler,
|
|
||||||
mouseEvent: "mousedown" | "mouseup" = "mousedown"
|
|
||||||
): void {
|
|
||||||
useEventListener(mouseEvent, (event) => {
|
|
||||||
const el = ref?.current;
|
|
||||||
// Do nothing if clicking ref's element or descendent elements
|
|
||||||
if (!el || el.contains(event.target as Node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handler(event);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
export type ColorPickerProps = {
|
|
||||||
defaultValue: string;
|
|
||||||
onChange: (text: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ColorPicker = (props: ColorPickerProps) => {
|
|
||||||
const init = !isValidHexCode(props.defaultValue)
|
|
||||||
? fallBackHex(props.defaultValue, false)
|
|
||||||
: props.defaultValue;
|
|
||||||
const [color, setColor] = useState(init);
|
|
||||||
const [isOpen, toggle] = useState(false);
|
|
||||||
const popover = useRef() as React.MutableRefObject<HTMLInputElement>;
|
|
||||||
const close = useCallback(() => toggle(false), []);
|
|
||||||
useOnClickOutside(popover, close);
|
|
||||||
return (
|
|
||||||
<div className="relative mt-1 flex items-center justify-center">
|
|
||||||
<Swatch size="sm" backgroundColor={color} onClick={() => toggle(!isOpen)} />
|
|
||||||
|
|
||||||
{isOpen && (
|
|
||||||
<div className="popover" ref={popover}>
|
|
||||||
<HexColorPicker
|
|
||||||
className="!absolute !top-10 !left-0 !z-10 !h-32 !w-32"
|
|
||||||
color={color}
|
|
||||||
onChange={(val) => {
|
|
||||||
setColor(val);
|
|
||||||
props.onChange(val);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<HexColorInput
|
|
||||||
className="ml-1 block w-full rounded-r-md border border-gray-300 px-3 py-2 sm:text-sm"
|
|
||||||
color={color}
|
|
||||||
onChange={(val) => {
|
|
||||||
setColor(val);
|
|
||||||
props.onChange(val);
|
|
||||||
}}
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ColorPicker;
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { forwardRef, InputHTMLAttributes } from "react";
|
import type { InputHTMLAttributes } from "react";
|
||||||
|
import React, { forwardRef } from "react";
|
||||||
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Props } from "react-select";
|
import type { Props } from "react-select";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Avatar } from "@calcom/ui";
|
import { Avatar } from "@calcom/ui";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { components, GroupBase, Props, SingleValue } from "react-select";
|
import type { GroupBase, Props, SingleValue } from "react-select";
|
||||||
|
import { components } from "react-select";
|
||||||
|
|
||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
@@ -39,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}>
|
||||||
|
|||||||
Loaded 100 of 626 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user