Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96308cd607 | ||
|
|
91464fb5e5 | ||
|
|
49d6dd9802 | ||
|
|
54cefcb16e | ||
|
|
f20d78bec1 | ||
|
|
089f52b425 | ||
|
|
47bf6d27d9 | ||
|
|
2cc937ba87 | ||
|
|
a57519040e | ||
|
|
3924731266 | ||
|
|
74986bb6a3 | ||
|
|
9568e16ef6 | ||
|
|
e88716758e | ||
|
|
fcb06a131b | ||
|
|
d778fe6a1b | ||
|
|
877f72d11a | ||
|
|
f728272c7f | ||
|
|
84fd277a97 | ||
|
|
ae0ed4176b | ||
|
|
ec4228ab6a | ||
|
|
19b1f8a05f | ||
|
|
1ba6b08edf | ||
|
|
f027f018ff | ||
|
|
9fd7ca65b5 | ||
|
|
c0f4c8e035 | ||
|
|
ed750c8df1 | ||
|
|
d545a9e127 | ||
|
|
6c55138589 | ||
|
|
18414e857a | ||
|
|
1c8f9cb3fd | ||
|
|
0871314d53 | ||
|
|
02d70c63a5 | ||
|
|
71a3374930 | ||
|
|
a22e4eed32 | ||
|
|
1625f462c2 | ||
|
|
5560f9def7 | ||
|
|
14c38f5108 | ||
|
|
229aeeb0c9 | ||
|
|
6a9704349d | ||
|
|
d2ffa08c83 | ||
|
|
2317473545 | ||
|
|
d81d772cdf | ||
|
|
cbc9cd60d5 | ||
|
+24 |
eb66931366 | ||
|
|
164f8918f5 | ||
|
|
7b21839c73 | ||
|
|
f3360e6476 | ||
|
|
e24f1d8bdb | ||
|
|
d50473468a | ||
|
|
4e8653cf4e | ||
|
|
7edebbcef0 | ||
|
|
f22bf721f2 | ||
|
|
ba0e443c52 | ||
|
|
aebfc7362e | ||
|
|
6d82e3f1b6 | ||
|
|
dbf74890a5 | ||
|
|
95c1c81b3b | ||
|
|
2305b926cd | ||
|
|
d9a555d94a | ||
|
|
943142e77f | ||
|
|
4ea729ca53 | ||
|
|
fa383d671b | ||
|
|
77bfd3773b | ||
|
|
4cd32fca4a | ||
|
|
c374f4fa20 | ||
|
|
2ddada49f9 | ||
|
|
feb8218e4e | ||
|
|
df4fedd61d | ||
|
|
d80581ad58 | ||
|
|
65687d6110 | ||
|
|
b3c973e824 | ||
|
|
913466d06d | ||
|
|
18c1a6a3a2 | ||
|
|
fe17203cea | ||
|
|
3a6f05f1e3 | ||
|
|
5ebbd514d8 | ||
|
|
58b439ca65 | ||
|
|
c32aadf297 | ||
|
|
736c30f134 | ||
|
|
7b169ebfc2 | ||
|
|
8f5843ab95 | ||
|
|
57d17b74c3 | ||
|
|
d40b934866 | ||
|
|
2e5c0c6332 | ||
|
|
b03bcc7470 | ||
|
|
2da5b19a76 | ||
|
|
0ee3b26ca8 | ||
|
|
e836a042e9 | ||
|
|
37038a0cbf | ||
|
|
8b47c064ff |
@@ -15,6 +15,7 @@
|
||||
# - LARK
|
||||
# - WEB3
|
||||
# - SALESFORCE
|
||||
# - ZOHOCRM
|
||||
|
||||
# - APP STORE **********************************************************************************************
|
||||
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
|
||||
@@ -109,4 +110,10 @@ INFURA_API_KEY=""
|
||||
# Used for the Salesforce (Sales Cloud) app
|
||||
SALESFORCE_CONSUMER_KEY=""
|
||||
SALESFORCE_CONSUMER_SECRET=""
|
||||
|
||||
# - ZOHOCRM
|
||||
# Used for the Zoho CRM integration
|
||||
ZOHOCRM_CLIENT_ID=""
|
||||
ZOHOCRM_CLIENT_SECRET=""
|
||||
|
||||
# *********************************************************************************************************
|
||||
|
||||
@@ -22,6 +22,10 @@ CALCOM_LICENSE_KEY=
|
||||
|
||||
# - DATABASE ************************************************************************************************
|
||||
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||
# Opt-in to a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||
# Cold boots will be faster and you will be able to scale your DB independently of your application.
|
||||
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
|
||||
PRISMA_GENERATE_DATAPROXY=false
|
||||
# ***********************************************************************************************************
|
||||
|
||||
# - SHARED **************************************************************************************************
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
**/**/node_modules
|
||||
**/**/.next
|
||||
**/**/public
|
||||
packages/prisma/zod
|
||||
apps/web/public/embed
|
||||
+1
-1
@@ -30,4 +30,4 @@ packages/features/ee/workflows @CarinaWolli @calcom/core
|
||||
packages/features/tips @PeerRich @calcom/core
|
||||
packages/ui @calcom/ui
|
||||
packages/config @calcom/core
|
||||
packages/app-store/ee/routing-forms @hariombalhara @calcom/core
|
||||
packages/app-store/routing-forms @hariombalhara @calcom/core
|
||||
|
||||
@@ -3,7 +3,7 @@ description: "Cache or restore if necessary"
|
||||
inputs:
|
||||
node_version:
|
||||
required: false
|
||||
default: v16.x
|
||||
default: v18.x
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
@@ -20,6 +20,7 @@ runs:
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/apps/web/.next
|
||||
${{ github.workspace }}/apps/web/public/embed
|
||||
**/.turbo/**
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
name: Yarn install
|
||||
description: "Install all NPM dependencies, caches them and restores if necessary"
|
||||
########################################################################################
|
||||
# "yarn install" composite action for yarn 2/3/4+ and "nodeLinker: node-modules" #
|
||||
#--------------------------------------------------------------------------------------#
|
||||
# Cache: #
|
||||
# - Downloaded zip archive (multi-arch, preserved across yarn.lock changes) #
|
||||
# - Yarn install state (discarded on yarn.lock changes) #
|
||||
# References: #
|
||||
# - bench: https://gist.github.com/belgattitude/0ecd26155b47e7be1be6163ecfbb0f0b #
|
||||
# - vs @setup/node: https://github.com/actions/setup-node/issues/325 #
|
||||
########################################################################################
|
||||
|
||||
name: "Yarn install"
|
||||
description: "Run yarn install with node_modules linker and cache enabled"
|
||||
inputs:
|
||||
node_version:
|
||||
required: false
|
||||
default: v16.x
|
||||
default: v18.x
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
@@ -11,9 +23,40 @@ runs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.node_version }}
|
||||
cache: "yarn"
|
||||
- name: Yarn install
|
||||
- name: Expose yarn config as "$GITHUB_OUTPUT"
|
||||
id: yarn-config
|
||||
shell: bash
|
||||
run: |
|
||||
yarn install --prefer-offline --frozen-lockfile
|
||||
echo "CACHE_FOLDER=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||
|
||||
# Yarn rotates the downloaded cache archives, @see https://github.com/actions/setup-node/issues/325
|
||||
# Yarn cache is also reusable between arch and os.
|
||||
- name: Restore yarn cache
|
||||
uses: actions/cache@v3
|
||||
id: yarn-download-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
|
||||
key: yarn-download-cache-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
yarn-download-cache-
|
||||
|
||||
# Invalidated on yarn.lock changes
|
||||
- name: Restore yarn install state
|
||||
id: yarn-install-state-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .yarn/ci-cache/
|
||||
key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
yarn install --inline-builds
|
||||
yarn prisma generate
|
||||
env:
|
||||
# CI optimizations. Overrides yarnrc.yml options (or their defaults) in the CI action.
|
||||
YARN_ENABLE_IMMUTABLE_INSTALLS: "false" # So it doesn't try to remove our private submodule deps
|
||||
YARN_ENABLE_GLOBAL_CACHE: "false" # Use local cache folder to keep downloaded archives
|
||||
YARN_INSTALL_STATE_PATH: .yarn/ci-cache/install-state.gz # Very small speedup when lock does not change
|
||||
# Other environment variables
|
||||
HUSKY: "0" # By default do not run HUSKY install
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
name: Cleanup Preview Databases
|
||||
|
||||
env:
|
||||
SNAPLET_ACCESS_TOKEN: ${{ secrets.SNAPLET_ACCESS_TOKEN }}
|
||||
SNAPLET_PROJECT_ID: cl4u26bwz7962859ply7ibuo43t
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
delete:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: snaplet/action@v1
|
||||
if: ${{ env.SNAPLET_ACCESS_TOKEN && env.SNAPLET_PROJECT_ID }}
|
||||
with:
|
||||
delete: true
|
||||
@@ -18,5 +18,3 @@ jobs:
|
||||
days-before-stale: 60
|
||||
include-only-assigned: true
|
||||
days-before-close: -1
|
||||
|
||||
|
||||
|
||||
@@ -17,11 +17,14 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@1.4.9
|
||||
uses: crowdin/github-action@1.5.1
|
||||
with:
|
||||
upload_translations: true
|
||||
upload_sources: true
|
||||
download_translations: true
|
||||
pull_request_labels: "♻️ autoupdate, automerge"
|
||||
push_translations: true
|
||||
commit_message: "New Crowdin translations by Github Action"
|
||||
localization_branch_name: main
|
||||
create_pull_request: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# .github/workflows/preview.yml
|
||||
|
||||
name: Preview Environment
|
||||
|
||||
env:
|
||||
SNAPLET_ACCESS_TOKEN: ${{ secrets.SNAPLET_ACCESS_TOKEN }}
|
||||
SNAPLET_PROJECT_ID: ${{ secrets.SNAPLET_PROJECT_ID }}
|
||||
VERCEL_ACCESS_TOKEN: ${{ secrets.VERCEL_ACCESS_TOKEN }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, closed]
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
db-preview-deploy:
|
||||
if: ${{ github.event.action == 'opened' || github.event.action == 'synchronize' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: snaplet
|
||||
uses: snaplet/action@v1
|
||||
- uses: snaplet/vercel-action@v1
|
||||
with:
|
||||
env: |
|
||||
DATABASE_URL=${{ steps.snaplet.outputs.database-url }}
|
||||
db-preview-delete:
|
||||
if: ${{ github.event.action == 'closed' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: snaplet/action@v1
|
||||
with:
|
||||
delete: true
|
||||
- uses: snaplet/vercel-action@v1
|
||||
with:
|
||||
delete: true
|
||||
@@ -1,18 +0,0 @@
|
||||
name: Staging DB Snapshot
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 4 * * 1-5" # At 04:00 on every day-of-week from Monday through Friday.
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
db-staging-snapshot:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Snaplet CLI
|
||||
run: curl -sL https://app.snaplet.dev/get-cli/ | bash
|
||||
- name: Restore Snapshot
|
||||
run: snaplet snapshot restore -y
|
||||
env:
|
||||
SNAPLET_DATABASE_URL: ${{ secrets.SNAPLET_DATA_TARGET_DB_URL }}
|
||||
SNAPLET_ACCESS_TOKEN: ${{ secrets.SNAPLET_ACCESS_TOKEN }}
|
||||
SNAPLET_DATABASE_ID: ${{ secrets.SNAPLET_DATABASE_ID }}
|
||||
@@ -0,0 +1,36 @@
|
||||
name: E2E Embed tests and booking flow(for non-embed as well)
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
e2e-embed:
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12.1
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: calendso
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- uses: ./.github/actions/env-read-file
|
||||
- uses: ./.github/actions/cache-db
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: yarn test-e2e:embed-react
|
||||
env:
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results
|
||||
@@ -25,19 +25,12 @@ jobs:
|
||||
- uses: ./.github/actions/cache-db
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: yarn turbo run embed-tests-update-snapshots:ci --scope=@calcom/embed-react --concurrency=1
|
||||
run: yarn test-e2e:embed
|
||||
env:
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
- name: Upload embed-core results
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: test-results-embed-core
|
||||
path: packages/embeds/embed-core/playwright/results
|
||||
|
||||
- name: Upload embed-react results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: test-results-embed-react
|
||||
path: packages/embeds/embed-react/playwright/results
|
||||
name: test-results
|
||||
path: test-results
|
||||
|
||||
@@ -20,7 +20,7 @@ env:
|
||||
INPUT_ENV_PAYMENT_FEE_FIXED: 10
|
||||
INPUT_ENV_SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||
INPUT_ENV_SAML_ADMINS: pro@example.com
|
||||
INPUT_ENV_NEXTAUTH_URL: http://localhost:3000/api/auth
|
||||
INPUT_ENV_NEXTAUTH_URL: http://127.0.0.1:3000/api/auth
|
||||
INPUT_ENV_NEXT_PUBLIC_IS_E2E: 1
|
||||
# INPUT_ENV_EMAIL_FROM: e2e@cal.com
|
||||
# INPUT_ENV_EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
||||
@@ -39,6 +39,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ozaytsev86/create-env-file@v1
|
||||
with:
|
||||
file-name: ${{ github.workspace }}/gh.env
|
||||
|
||||
@@ -21,6 +21,7 @@ jobs:
|
||||
outputs:
|
||||
app-store: ${{ steps.filter.outputs.app-store }}
|
||||
embed: ${{ steps.filter.outputs.embed }}
|
||||
embed-react: ${{ steps.filter.outputs.embed-react }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
@@ -34,6 +35,9 @@ jobs:
|
||||
embed:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
embed-react:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
|
||||
env:
|
||||
name: Create env file
|
||||
@@ -81,6 +85,13 @@ jobs:
|
||||
uses: ./.github/workflows/e2e-embed.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed-react:
|
||||
name: E2E React embeds tests
|
||||
if: ${{ needs.changes.outputs.embed-react == 'true' }}
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
secrets: inherit
|
||||
|
||||
analyze:
|
||||
needs: build
|
||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||
|
||||
+9
-3
@@ -77,9 +77,6 @@ lint-results
|
||||
#Storybook
|
||||
apps/storybook/build-storybook.log
|
||||
|
||||
# Infisical
|
||||
.infisical.json
|
||||
|
||||
# Snaplet
|
||||
.snaplet/snapshots
|
||||
.snaplet/structure.d.ts
|
||||
@@ -90,3 +87,12 @@ apps/api
|
||||
apps/website
|
||||
apps/console
|
||||
apps/auth
|
||||
|
||||
# Yarn Modern
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"workspaceId": "63ea8121b6e2b0543ba79616",
|
||||
"defaultEnvironment": "dev",
|
||||
"gitBranchToEnvironmentMapping": {
|
||||
"main": "staging",
|
||||
"production": "prod"
|
||||
}
|
||||
}
|
||||
@@ -14,3 +14,4 @@ public
|
||||
.DS_Store
|
||||
.eslintignore
|
||||
packages/prisma/zod
|
||||
apps/web/public/embed
|
||||
|
||||
+541
File diff suppressed because one or more lines are too long
+873
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.4.1.cjs
|
||||
+1
-2
@@ -4,7 +4,7 @@ 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.
|
||||
|
||||
## Priorities
|
||||
## Priorities
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
@@ -57,7 +57,6 @@ Contributions are what make the open source community such an amazing place to l
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## Developing
|
||||
|
||||
The development branch is `main`. This is the branch that all pull
|
||||
|
||||
@@ -350,17 +350,16 @@ We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:is
|
||||
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<!-- CONTRIBUTORS -->
|
||||
|
||||
### Contributors
|
||||
|
||||
|
||||
<a href="https://github.com/calcom/cal.com/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=calcom/cal.com" />
|
||||
</a>
|
||||
|
||||
|
||||
<!-- TRANSLATIONS -->
|
||||
|
||||
### Translations
|
||||
@@ -452,6 +451,19 @@ following
|
||||
9. Click the "Save" button at the bottom footer.
|
||||
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
|
||||
|
||||
### Obtaining ZohoCRM Client ID and Secret
|
||||
|
||||
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
|
||||
2. From within the API console page, go to "Applications".
|
||||
3. Click "ADD CLIENT" button top right and select "Server-based Applications".
|
||||
4. Fill in any information you want in the "Client Details" tab
|
||||
5. Go to tab "Client Secret" tab.
|
||||
6. Now copy the Client ID and Client Secret to your .env file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields.
|
||||
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocrm/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||
8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
|
||||
9. Click the "Save"/ "UPDATE" button at the bottom footer.
|
||||
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
|
||||
|
||||
## Workflows
|
||||
|
||||
### Setting up SendGrid for Email reminders
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
14.17
|
||||
v18
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/config": "*",
|
||||
"eslint": "^8.34.0"
|
||||
"@calcom/config": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
node_modules
|
||||
prisma/zod
|
||||
public/embed
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
v16
|
||||
v18
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
public/embed
|
||||
@@ -64,7 +64,7 @@ export default function AppListCard(props: AppListCardProps) {
|
||||
|
||||
return (
|
||||
<div className={`${highlight ? "bg-yellow-100" : ""}`}>
|
||||
<div className="flex gap-x-3 p-4">
|
||||
<div className="flex gap-x-3 px-5 py-4">
|
||||
{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 items-center gap-x-2">
|
||||
|
||||
@@ -669,8 +669,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
embedUrl: string;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const router = useRouter();
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const dialogContentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const s = (href: string) => {
|
||||
const searchParams = new URLSearchParams(router.asPath.split("?")[1] || "");
|
||||
const [a, b] = href.split("=");
|
||||
@@ -809,7 +812,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
];
|
||||
|
||||
return (
|
||||
<DialogContent size="xl" className="p-0.5" type="creation">
|
||||
<DialogContent ref={dialogContentRef} size="xl" className="p-0.5" type="creation">
|
||||
<div className="flex">
|
||||
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
|
||||
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
|
||||
@@ -960,6 +963,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
<div>Button Color</div>
|
||||
<div className="w-full">
|
||||
<ColorPicker
|
||||
popoverAlign="start"
|
||||
container={dialogContentRef?.current ?? undefined}
|
||||
defaultValue="#000000"
|
||||
onChange={(color) => {
|
||||
setPreviewState((previewState) => {
|
||||
@@ -979,6 +984,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
<div>Text Color</div>
|
||||
<div className="w-full">
|
||||
<ColorPicker
|
||||
popoverAlign="start"
|
||||
container={dialogContentRef?.current ?? undefined}
|
||||
defaultValue="#000000"
|
||||
onChange={(color) => {
|
||||
setPreviewState((previewState) => {
|
||||
@@ -1060,6 +1067,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
<div className="mb-2 pt-2">{palette.title}</div>
|
||||
<div className="w-full">
|
||||
<ColorPicker
|
||||
popoverAlign="start"
|
||||
container={dialogContentRef?.current ?? undefined}
|
||||
defaultValue="#000000"
|
||||
onChange={(color) => {
|
||||
addToPalette({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { InstallAppButton, AppDependencyComponent } from "@calcom/app-store/components";
|
||||
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
FiShield,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
/* These app slugs all require Google Cal to be installed */
|
||||
|
||||
const Component = ({
|
||||
name,
|
||||
type,
|
||||
@@ -45,6 +47,7 @@ const Component = ({
|
||||
isProOnly,
|
||||
images,
|
||||
isTemplate,
|
||||
dependencies,
|
||||
}: Parameters<typeof App>[0]) => {
|
||||
const { t } = useLocale();
|
||||
const hasImages = images && images.length > 0;
|
||||
@@ -76,6 +79,15 @@ const Component = ({
|
||||
}
|
||||
);
|
||||
|
||||
const dependencyData = trpc.viewer.appsRouter.queryForDependencies.useQuery(dependencies, {
|
||||
enabled: !!dependencies,
|
||||
});
|
||||
|
||||
const disableInstall =
|
||||
dependencyData.data && dependencyData.data.some((dependency) => !dependency.installed);
|
||||
|
||||
// const disableInstall = requiresGCal && !gCalInstalled.data;
|
||||
|
||||
// variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal
|
||||
// Such apps, can only be installed once.
|
||||
const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other";
|
||||
@@ -137,6 +149,7 @@ const Component = ({
|
||||
<InstallAppButton
|
||||
type={type}
|
||||
isProOnly={isProOnly}
|
||||
disableInstall={disableInstall}
|
||||
render={({ useDefaultComponent, ...props }) => {
|
||||
if (useDefaultComponent) {
|
||||
props = {
|
||||
@@ -176,6 +189,7 @@ const Component = ({
|
||||
<InstallAppButton
|
||||
type={type}
|
||||
isProOnly={isProOnly}
|
||||
disableInstall={disableInstall}
|
||||
render={({ useDefaultComponent, ...props }) => {
|
||||
if (useDefaultComponent) {
|
||||
props = {
|
||||
@@ -203,6 +217,16 @@ const Component = ({
|
||||
) : (
|
||||
<SkeletonButton className="h-10 w-24" />
|
||||
)}
|
||||
|
||||
{dependencies &&
|
||||
(!dependencyData.isLoading ? (
|
||||
<div className="mt-6">
|
||||
<AppDependencyComponent appName={name} dependencyData={dependencyData.data} />
|
||||
</div>
|
||||
) : (
|
||||
<SkeletonButton className="mt-6 h-20 grow" />
|
||||
))}
|
||||
|
||||
{price !== 0 && (
|
||||
<span className="block text-right">
|
||||
{feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar}
|
||||
@@ -332,6 +356,8 @@ export default function App(props: {
|
||||
isProOnly: AppType["isProOnly"];
|
||||
images?: string[];
|
||||
isTemplate?: boolean;
|
||||
disableInstall?: boolean;
|
||||
dependencies?: string[];
|
||||
}) {
|
||||
return (
|
||||
<Shell smallHeading isPublic heading={<ShellHeading />} backPath="/apps" withoutSeo>
|
||||
|
||||
@@ -88,7 +88,7 @@ function CalendarSwitch(props: {
|
||||
}
|
||||
);
|
||||
return (
|
||||
<div className="flex py-1">
|
||||
<div className="flex flex-col py-1 sm:flex-row">
|
||||
<Switch
|
||||
key={props.externalId}
|
||||
name="enabled"
|
||||
@@ -99,7 +99,7 @@ function CalendarSwitch(props: {
|
||||
}}
|
||||
/>
|
||||
{!!props.destination && (
|
||||
<span className="ml-4 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800">
|
||||
<span className="ml-8 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800 sm:ml-4">
|
||||
<FiArrowLeft className="h-4 w-4" />
|
||||
{t("adding_events_to")}
|
||||
</span>
|
||||
@@ -184,8 +184,8 @@ function ConnectedCalendarsList(props: Props) {
|
||||
<div className="border-t border-gray-200">
|
||||
{!fromOnboarding && (
|
||||
<>
|
||||
<p className="px-4 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
|
||||
<ul className="space-y-2 p-4">
|
||||
<p className="px-5 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
|
||||
<ul className="space-y-2 px-5 py-4">
|
||||
{item.calendars.map((cal) => (
|
||||
<CalendarSwitch
|
||||
key={cal.externalId}
|
||||
@@ -264,7 +264,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
|
||||
<>
|
||||
{heading && (
|
||||
<div className="flex flex-col gap-6 rounded-md border border-gray-200 p-7">
|
||||
<div className="flex flex-col gap-6 rounded-md border p-7">
|
||||
<ShellSubHeading
|
||||
title={t("calendar")}
|
||||
subtitle={t("installed_app_calendar_description")}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
||||
return (
|
||||
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||
{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 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
||||
) : (
|
||||
<img
|
||||
src={eventLocationType.iconUrl}
|
||||
|
||||
@@ -24,6 +24,7 @@ type AvailableTimesProps = {
|
||||
eventTypeSlug: string;
|
||||
date?: Dayjs;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
bookingAttendees?: number | null;
|
||||
slots?: Slot[];
|
||||
isLoading: boolean;
|
||||
ethSignature?: string;
|
||||
@@ -39,6 +40,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
timeFormat,
|
||||
onTimeFormatChange,
|
||||
seatsPerTimeSlot,
|
||||
bookingAttendees,
|
||||
ethSignature,
|
||||
}) => {
|
||||
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
||||
@@ -64,13 +66,13 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
return (
|
||||
<div ref={slotPickerRef}>
|
||||
{!!date ? (
|
||||
<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 className="mb-6 flex items-center text-left text-base">
|
||||
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col rounded-md px-4 text-center sm:mt-0 sm:p-4 md:-mb-5 md:min-w-[200px] md:p-4 lg:min-w-[300px]">
|
||||
<div className="mb-4 flex items-center text-left text-base">
|
||||
<div className="mr-4">
|
||||
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
|
||||
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||
</span>
|
||||
<span className="text-bookinglight font-medium">
|
||||
<span className="text-bookinglight">
|
||||
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
||||
</span>
|
||||
</div>
|
||||
@@ -85,7 +87,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref={ref} className="flex-grow overflow-y-auto sm:block md:h-[364px]">
|
||||
<div
|
||||
ref={ref}
|
||||
className="scroll-bar scrollbar-track-w-20 relative -mb-4 flex-grow overflow-y-auto sm:block md:h-[364px]">
|
||||
{slots.length > 0 &&
|
||||
slots.map((slot) => {
|
||||
type BookingURL = {
|
||||
@@ -114,18 +118,28 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
bookingUrl.query.bookingUid = slot.bookingUid;
|
||||
}
|
||||
|
||||
let slotFull, notEnoughSeats;
|
||||
|
||||
if (slot.attendees && seatsPerTimeSlot) slotFull = slot.attendees >= seatsPerTimeSlot;
|
||||
if (slot.attendees && bookingAttendees && seatsPerTimeSlot)
|
||||
notEnoughSeats = slot.attendees + bookingAttendees > seatsPerTimeSlot;
|
||||
|
||||
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 ? (
|
||||
{seatsPerTimeSlot && slot.attendees && (slotFull || notEnoughSeats) ? (
|
||||
<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>}
|
||||
{notEnoughSeats ? (
|
||||
<p className="text-sm">{t("not_enough_seats")}</p>
|
||||
) : slots ? (
|
||||
<p className="text-sm">{t("booking_full")}</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
|
||||
@@ -46,14 +46,16 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
const { profile, eventType, isBookingPage = false, children } = props;
|
||||
const { date: bookingDate } = useRouterQuery("date");
|
||||
const { t } = useLocale();
|
||||
const { duration = eventType.length.toString(), setQuery: setDuration } = useRouterQuery("duration");
|
||||
const { duration, setQuery: setDuration } = useRouterQuery("duration");
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
eventType.metadata?.multipleDuration &&
|
||||
!eventType.metadata?.multipleDuration?.includes(Number(duration))
|
||||
!duration ||
|
||||
isNaN(Number(duration)) ||
|
||||
(eventType.metadata?.multipleDuration &&
|
||||
!eventType.metadata?.multipleDuration.includes(Number(duration)))
|
||||
) {
|
||||
setDuration(eventType.length.toString());
|
||||
setDuration(eventType.length);
|
||||
}
|
||||
}, [duration, setDuration, eventType.length, eventType.metadata?.multipleDuration]);
|
||||
|
||||
@@ -81,20 +83,20 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
size="sm"
|
||||
truncateAfter={3}
|
||||
/>
|
||||
<h2 className="mt-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||
<h2 className="mt-1 mb-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||
{profile.name}
|
||||
</h2>
|
||||
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold text-gray-900">
|
||||
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold leading-none text-gray-900">
|
||||
{eventType.title}
|
||||
</h1>
|
||||
<div className="dark:text-darkgray-600 flex flex-col space-y-4 text-sm font-medium text-gray-600">
|
||||
{eventType?.description && (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex",
|
||||
"scroll-bar scrollbar-track-w-20 -mx-5 flex max-h-[180px] overflow-y-scroll px-5 ",
|
||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
||||
)}>
|
||||
<div className="scroll-bar scrollbar-track-w-20 max-h-[200px] max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
|
||||
<div className="max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
|
||||
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,6 +94,13 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
mutation.mutate(body);
|
||||
};
|
||||
|
||||
const getSeatReferenceUid = () => {
|
||||
if (!booking.seatsReferences[0]) {
|
||||
return undefined;
|
||||
}
|
||||
return booking.seatsReferences[0].referenceUid;
|
||||
};
|
||||
|
||||
const pendingActions: ActionType[] = [
|
||||
{
|
||||
id: "reject",
|
||||
@@ -135,7 +142,8 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
cancel all remaining bookings or just that booking instance. */
|
||||
href: `/booking/${booking.uid}?cancel=true${
|
||||
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
||||
}`,
|
||||
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
|
||||
`,
|
||||
icon: FiX,
|
||||
},
|
||||
{
|
||||
@@ -146,7 +154,9 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
id: "reschedule",
|
||||
icon: FiClock,
|
||||
label: t("reschedule_booking"),
|
||||
href: `/reschedule/${booking.uid}`,
|
||||
href: `/reschedule/${booking.uid}${
|
||||
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
||||
}`,
|
||||
},
|
||||
{
|
||||
id: "reschedule_request",
|
||||
@@ -179,10 +189,9 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
|
||||
const RequestSentMessage = () => {
|
||||
return (
|
||||
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
||||
<FiSend className="-mt-[1px] w-4 rotate-45" />
|
||||
<p className="ml-2 ">{t("reschedule_request_sent")}</p>
|
||||
</div>
|
||||
<Badge startIcon={FiSend} size="md" variant="gray" data-testid="request_reschedule_sent">
|
||||
{t("reschedule_request_sent")}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -224,7 +233,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
const showRecordingsButtons =
|
||||
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
||||
|
||||
const title = decodeURIComponent(booking.title);
|
||||
const title = booking.title;
|
||||
return (
|
||||
<>
|
||||
<RescheduleDialog
|
||||
@@ -386,13 +395,13 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
/>
|
||||
)}
|
||||
{isCancelled && booking.rescheduled && (
|
||||
<div className="mt-2 inline-block text-left text-sm md:hidden">
|
||||
<div className="mt-2 inline-block md:hidden">
|
||||
<RequestSentMessage />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:pl-0">
|
||||
<td className="flex w-full justify-end py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:pl-0">
|
||||
{isUpcoming && !isCancelled ? (
|
||||
<>
|
||||
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
@@ -23,18 +23,26 @@ type Props = {
|
||||
setIsCancellationMode: (value: boolean) => void;
|
||||
theme: string | null;
|
||||
allRemainingBookings: boolean;
|
||||
seatReferenceUid?: string;
|
||||
};
|
||||
|
||||
export default function CancelBooking(props: Props) {
|
||||
const [cancellationReason, setCancellationReason] = useState<string>("");
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const { booking, allRemainingBookings } = props;
|
||||
const { booking, allRemainingBookings, seatReferenceUid } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const telemetry = useTelemetry();
|
||||
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
|
||||
useTheme(props.theme);
|
||||
|
||||
const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
|
||||
if (node !== null) {
|
||||
node.scrollIntoView({ behavior: "smooth" });
|
||||
node.focus();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
{error && (
|
||||
@@ -53,6 +61,7 @@ export default function CancelBooking(props: Props) {
|
||||
<div className="mt-5 sm:mt-6">
|
||||
<label className="text-bookingdark font-medium dark:text-white">{t("cancellation_reason")}</label>
|
||||
<TextArea
|
||||
ref={cancelBookingRef}
|
||||
placeholder={t("cancellation_reason_placeholder")}
|
||||
value={cancellationReason}
|
||||
onChange={(e) => setCancellationReason(e.target.value)}
|
||||
@@ -73,16 +82,16 @@ export default function CancelBooking(props: Props) {
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
|
||||
const payload = {
|
||||
id: booking?.id,
|
||||
cancellationReason: cancellationReason,
|
||||
allRemainingBookings,
|
||||
};
|
||||
|
||||
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
|
||||
|
||||
const res = await fetch("/api/cancel", {
|
||||
body: JSON.stringify(payload),
|
||||
body: JSON.stringify({
|
||||
id: booking?.id,
|
||||
cancellationReason: cancellationReason,
|
||||
allRemainingBookings,
|
||||
// @NOTE: very important this shouldn't cancel with number ID use uid instead
|
||||
seatReferenceUid,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
|
||||
@@ -78,6 +78,7 @@ export const SlotPicker = ({
|
||||
recurringEventCount,
|
||||
users,
|
||||
seatsPerTimeSlot,
|
||||
bookingAttendees,
|
||||
weekStart = 0,
|
||||
ethSignature,
|
||||
}: {
|
||||
@@ -89,6 +90,7 @@ export const SlotPicker = ({
|
||||
onTimeFormatChange: (is24Hour: boolean) => void;
|
||||
timeZone?: string;
|
||||
seatsPerTimeSlot?: number;
|
||||
bookingAttendees?: number;
|
||||
recurringEventCount?: number;
|
||||
users: string[];
|
||||
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
@@ -164,7 +166,7 @@ export const SlotPicker = ({
|
||||
<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]",
|
||||
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-4 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)}
|
||||
@@ -192,6 +194,7 @@ export const SlotPicker = ({
|
||||
eventTypeId={eventType.id}
|
||||
eventTypeSlug={eventType.slug}
|
||||
seatsPerTimeSlot={seatsPerTimeSlot}
|
||||
bookingAttendees={bookingAttendees}
|
||||
recurringCount={recurringEventCount}
|
||||
ethSignature={ethSignature}
|
||||
/>
|
||||
|
||||
@@ -17,7 +17,7 @@ export function TimezoneDropdown({
|
||||
|
||||
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 max-w-[20rem] items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white">
|
||||
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit max-w-[20rem] items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]: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>
|
||||
|
||||
@@ -23,8 +23,9 @@ import notEmpty from "@calcom/lib/notEmpty";
|
||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
import { FiCreditCard, FiRefreshCcw } from "@calcom/ui/components/icon";
|
||||
import { FiCreditCard, FiUser, FiRefreshCcw } from "@calcom/ui/components/icon";
|
||||
|
||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||
|
||||
@@ -52,6 +53,7 @@ const dateQuerySchema = z.object({
|
||||
rescheduleUid: z.string().optional().default(""),
|
||||
date: z.string().optional().default(""),
|
||||
timeZone: z.string().optional().default(""),
|
||||
seatReferenceUid: z.string().optional(),
|
||||
});
|
||||
|
||||
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
|
||||
@@ -124,6 +126,15 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
: undefined,
|
||||
];
|
||||
|
||||
const { data: bookingAttendees } = trpc.viewer.bookings.getBookingAttendees.useQuery(
|
||||
{
|
||||
seatReferenceUid: rescheduleUid,
|
||||
},
|
||||
{
|
||||
enabled: !!(rescheduleUid && eventType.seatsPerTimeSlot),
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Gates gates={gates} appData={rainbowAppData} dispatch={gateDispatcher}>
|
||||
<HeadSeo
|
||||
@@ -168,10 +179,26 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
<div
|
||||
className={classNames(
|
||||
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
|
||||
"min-w-full md:w-[230px] md:min-w-[230px]",
|
||||
"min-w-full md:w-[280px] md:min-w-[280px]",
|
||||
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
|
||||
)}>
|
||||
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
||||
{rescheduleUid && eventType.seatsPerTimeSlot && bookingAttendees && (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-nowrap items-center text-sm font-medium",
|
||||
"dark:text-darkgray-600 text-gray-600",
|
||||
"ltr:mr-[10px] rtl:ml-[10px]"
|
||||
)}>
|
||||
<FiUser
|
||||
className={classNames(
|
||||
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
|
||||
"mt-[2px]"
|
||||
)}
|
||||
/>{" "}
|
||||
{t("event_type_seats", { numberOfSeats: bookingAttendees })}
|
||||
</div>
|
||||
)}
|
||||
{!rescheduleUid && eventType.recurringEvent && (
|
||||
<div className="flex items-start text-sm font-medium">
|
||||
<FiRefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||
@@ -247,6 +274,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
timeZone={timeZone}
|
||||
users={userList}
|
||||
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
|
||||
bookingAttendees={bookingAttendees || undefined}
|
||||
recurringEventCount={recurringEventCount}
|
||||
ethSignature={gateState.rainbowToken}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useSession } from "next-auth/react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useReducer, useState } from "react";
|
||||
import { useForm, useFormContext } from "react-hook-form";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
@@ -15,6 +15,7 @@ import { getEventLocationType, locationKeyToString } from "@calcom/app-store/loc
|
||||
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
||||
import type { LocationObject } from "@calcom/core/location";
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
useEmbedNonStylesConfig,
|
||||
@@ -38,6 +39,7 @@ import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Button, Form, Tooltip } from "@calcom/ui";
|
||||
import { FiAlertTriangle, FiCalendar, FiRefreshCw, FiUser } from "@calcom/ui/components/icon";
|
||||
|
||||
@@ -113,7 +115,10 @@ const BookingFields = ({
|
||||
}
|
||||
|
||||
// We don't show `notes` field during reschedule
|
||||
if (field.name === SystemField.Enum.notes && !!rescheduleUid) {
|
||||
if (
|
||||
(field.name === SystemField.Enum.notes || field.name === SystemField.Enum.guests) &&
|
||||
!!rescheduleUid
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -213,6 +218,20 @@ const BookingPage = ({
|
||||
duration = Number(queryDuration);
|
||||
}
|
||||
|
||||
// This is a workaround for forcing the same time format for both server side rendering and client side rendering
|
||||
// At initial render, we use the default time format which is 12H
|
||||
const [withDefaultTimeFormat, setWithDefaultTimeFormat] = useState(true);
|
||||
const parseDateFunc = useCallback(
|
||||
(date: string | null | Dayjs) => {
|
||||
return parseDate(date, i18n, withDefaultTimeFormat);
|
||||
},
|
||||
[withDefaultTimeFormat]
|
||||
);
|
||||
// After intial render on client side, we let parseDateFunc to use the time format from the localStorage
|
||||
useEffect(() => {
|
||||
setWithDefaultTimeFormat(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (top !== window) {
|
||||
//page_view will be collected automatically by _middleware.ts
|
||||
@@ -226,12 +245,12 @@ const BookingPage = ({
|
||||
|
||||
const mutation = useMutation(createBooking, {
|
||||
onSuccess: async (responseData) => {
|
||||
const { uid, paymentUid } = responseData;
|
||||
const { uid } = responseData;
|
||||
|
||||
if (paymentUid) {
|
||||
if ("paymentUid" in responseData && !!responseData.paymentUid) {
|
||||
return await router.push(
|
||||
createPaymentLink({
|
||||
paymentUid,
|
||||
paymentUid: responseData.paymentUid,
|
||||
date,
|
||||
name: bookingForm.getValues("responses.name"),
|
||||
email: bookingForm.getValues("responses.email"),
|
||||
@@ -246,6 +265,7 @@ const BookingPage = ({
|
||||
isSuccessBookingPage: true,
|
||||
email: bookingForm.getValues("responses.email"),
|
||||
eventTypeSlug: eventType.slug,
|
||||
seatReferenceUid: "seatReferenceUid" in responseData ? responseData.seatReferenceUid : null,
|
||||
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
|
||||
},
|
||||
});
|
||||
@@ -370,19 +390,30 @@ const BookingPage = ({
|
||||
// Calculate the booking date(s)
|
||||
let recurringStrings: string[] = [],
|
||||
recurringDates: Date[] = [];
|
||||
const parseRecurringDatesFunc = useCallback(
|
||||
(date: string | null | Dayjs, recurringEvent: RecurringEvent, recurringCount: number) => {
|
||||
return parseRecurringDates(
|
||||
{
|
||||
startDate: date,
|
||||
timeZone: timeZone(),
|
||||
recurringEvent: recurringEvent,
|
||||
recurringCount: recurringCount,
|
||||
withDefaultTimeFormat: withDefaultTimeFormat,
|
||||
},
|
||||
i18n
|
||||
);
|
||||
},
|
||||
[withDefaultTimeFormat, date, eventType.recurringEvent, recurringEventCount]
|
||||
);
|
||||
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
|
||||
[recurringStrings, recurringDates] = parseRecurringDates(
|
||||
{
|
||||
startDate: date,
|
||||
timeZone: timeZone(),
|
||||
recurringEvent: eventType.recurringEvent,
|
||||
recurringCount: parseInt(recurringEventCount.toString()),
|
||||
},
|
||||
i18n
|
||||
[recurringStrings, recurringDates] = parseRecurringDatesFunc(
|
||||
date,
|
||||
eventType.recurringEvent,
|
||||
parseInt(recurringEventCount.toString())
|
||||
);
|
||||
}
|
||||
|
||||
const bookEvent = (booking: BookingFormValues) => {
|
||||
const bookEvent = (bookingValues: BookingFormValues) => {
|
||||
telemetry.event(
|
||||
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
|
||||
{ isTeamBooking: document.URL.includes("team/") }
|
||||
@@ -405,7 +436,7 @@ const BookingPage = ({
|
||||
// Identify set of bookings to one intance of recurring event to support batch changes
|
||||
const recurringEventId = uuidv4();
|
||||
const recurringBookings = recurringDates.map((recurringDate) => ({
|
||||
...booking,
|
||||
...bookingValues,
|
||||
start: dayjs(recurringDate).format(),
|
||||
end: dayjs(recurringDate).add(duration, "minute").format(),
|
||||
eventTypeId: eventType.id,
|
||||
@@ -425,7 +456,7 @@ const BookingPage = ({
|
||||
recurringMutation.mutate(recurringBookings);
|
||||
} else {
|
||||
mutation.mutate({
|
||||
...booking,
|
||||
...bookingValues,
|
||||
start: dayjs(date).tz(timeZone()).format(),
|
||||
end: dayjs(date).tz(timeZone()).add(duration, "minute").format(),
|
||||
eventTypeId: eventType.id,
|
||||
@@ -433,12 +464,13 @@ const BookingPage = ({
|
||||
timeZone: timeZone(),
|
||||
language: i18n.language,
|
||||
rescheduleUid,
|
||||
bookingUid: router.query.bookingUid as string,
|
||||
bookingUid: (router.query.bookingUid as string) || booking?.uid,
|
||||
user: router.query.user,
|
||||
metadata,
|
||||
hasHashedBookingLink,
|
||||
hashedLink,
|
||||
ethSignature: gateState.rainbowToken,
|
||||
seatReferenceUid: router.query.seatReferenceUid as string,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -507,7 +539,7 @@ const BookingPage = ({
|
||||
<div className="text-sm font-medium">
|
||||
{isClientTimezoneAvailable &&
|
||||
(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
||||
`${parseDate(date, i18n)}`}
|
||||
`${parseDateFunc(date)}`}
|
||||
{isClientTimezoneAvailable &&
|
||||
!rescheduleUid &&
|
||||
eventType.recurringEvent?.freq &&
|
||||
@@ -537,7 +569,7 @@ const BookingPage = ({
|
||||
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
{isClientTimezoneAvailable &&
|
||||
typeof booking.startTime === "string" &&
|
||||
parseDate(dayjs(booking.startTime), i18n)}
|
||||
parseDateFunc(dayjs(booking.startTime))}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -615,17 +647,28 @@ export default BookingPage;
|
||||
function ErrorMessage({ error }: { error: unknown }) {
|
||||
const { t } = useLocale();
|
||||
const { query: { rescheduleUid } = {} } = useRouter();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div data-testid="booking-fail" className="mt-2 border-l-4 border-yellow-400 bg-yellow-50 p-4">
|
||||
<div data-testid="booking-fail" className="mt-2 border-l-4 border-blue-400 bg-blue-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<FiAlertTriangle className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||
<FiAlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ltr:ml-3 rtl:mr-3">
|
||||
<p className="text-sm text-yellow-700">
|
||||
<p className="text-sm text-blue-700">
|
||||
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
|
||||
{error instanceof HttpError || error instanceof Error ? t(error.message) : "Unknown error"}
|
||||
{error instanceof HttpError || error instanceof Error ? (
|
||||
<>
|
||||
{t("can_you_try_again")}{" "}
|
||||
<span className="cursor-pointer underline" onClick={() => router.back()}>
|
||||
{t("go_back")}
|
||||
</span>
|
||||
.
|
||||
</> /* t(error.message) */
|
||||
) : (
|
||||
"Unknown error"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,8 @@ const CustomEventTypeModalForm: FC<CustomEventTypeModalFormProps> = (props) => {
|
||||
type="text"
|
||||
placeholder={placeHolder_}
|
||||
{...register("customEventName", {
|
||||
validate: (value) => validateCustomEventName(value, t("invalid_event_name_variables")),
|
||||
validate: (value) =>
|
||||
validateCustomEventName(value, t("invalid_event_name_variables"), event.bookingFields),
|
||||
})}
|
||||
className="mb-0"
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ import DestinationCalendarSelector from "@calcom/features/calendars/DestinationC
|
||||
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
|
||||
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
|
||||
@@ -36,11 +37,19 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
||||
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
||||
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
||||
|
||||
const bookingFields: Prisma.JsonObject = {};
|
||||
|
||||
eventType.bookingFields.forEach(({ name }) => {
|
||||
bookingFields[name] = name + " input";
|
||||
});
|
||||
|
||||
const eventNameObject: EventNameObjectType = {
|
||||
attendeeName: t("scheduler"),
|
||||
eventType: eventType.title,
|
||||
eventName: eventType.eventName,
|
||||
host: eventType.users[0]?.name || "Nameless",
|
||||
bookingFields: bookingFields,
|
||||
t,
|
||||
};
|
||||
|
||||
@@ -255,6 +264,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
defaultValue={!!eventType.seatsPerTimeSlot}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SettingsToggle
|
||||
data-testid="offer-seats-toggle"
|
||||
title={t("offer_seats")}
|
||||
description={t("offer_seats_description")}
|
||||
checked={value}
|
||||
@@ -308,7 +318,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
<CustomEventTypeModal
|
||||
close={closeEventNameTip}
|
||||
setValue={setEventName}
|
||||
defaultValue={eventType.eventName || ""}
|
||||
defaultValue={formMethods.getValues("eventName") || eventType.eventName || ""}
|
||||
placeHolder={eventNamePlaceholder}
|
||||
event={eventNameObject}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import type { Key } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import type { UseFormRegisterReturn } from "react-hook-form";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import type { SingleValue } from "react-select";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
|
||||
@@ -11,8 +13,17 @@ import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
||||
import findDurationType from "@calcom/lib/findDurationType";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { PeriodType } from "@calcom/prisma/client";
|
||||
import type { BookingLimit } from "@calcom/types/Calendar";
|
||||
import { Button, DateRangePicker, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||
import {
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
InputField,
|
||||
Label,
|
||||
Select,
|
||||
SettingsToggle,
|
||||
TextField,
|
||||
} from "@calcom/ui";
|
||||
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
|
||||
|
||||
const MinimumBookingNoticeInput = React.forwardRef<
|
||||
@@ -260,7 +271,34 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
formMethods.setValue("bookingLimits", {});
|
||||
}
|
||||
}}>
|
||||
<BookingLimits />
|
||||
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
|
||||
</SettingsToggle>
|
||||
)}
|
||||
/>
|
||||
<hr />
|
||||
<Controller
|
||||
name="durationLimits"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<SettingsToggle
|
||||
title={t("limit_total_booking_duration")}
|
||||
description={t("limit_total_booking_duration_description")}
|
||||
checked={Object.keys(value ?? {}).length > 0}
|
||||
onCheckedChange={(active) => {
|
||||
if (active) {
|
||||
formMethods.setValue("durationLimits", {
|
||||
PER_DAY: 60,
|
||||
});
|
||||
} else {
|
||||
formMethods.setValue("durationLimits", {});
|
||||
}
|
||||
}}>
|
||||
<IntervalLimitsManager
|
||||
propertyName="durationLimits"
|
||||
defaultLimit={60}
|
||||
step={15}
|
||||
textFieldSuffix={t("minutes")}
|
||||
/>
|
||||
</SettingsToggle>
|
||||
)}
|
||||
/>
|
||||
@@ -348,124 +386,158 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
);
|
||||
};
|
||||
|
||||
const validationOrderKeys = ["PER_DAY", "PER_WEEK", "PER_MONTH", "PER_YEAR"];
|
||||
type BookingLimitsKey = keyof BookingLimit;
|
||||
const BookingLimits = () => {
|
||||
type IntervalLimitsKey = keyof IntervalLimit;
|
||||
|
||||
const intervalOrderKeys = ["PER_DAY", "PER_WEEK", "PER_MONTH", "PER_YEAR"] as const;
|
||||
|
||||
const INTERVAL_LIMIT_OPTIONS = intervalOrderKeys.map((key) => ({
|
||||
value: key as keyof IntervalLimit,
|
||||
label: `Per ${key.split("_")[1].toLocaleLowerCase()}`,
|
||||
}));
|
||||
|
||||
type IntervalLimitItemProps = {
|
||||
key: Key;
|
||||
limitKey: IntervalLimitsKey;
|
||||
step: number;
|
||||
value: number;
|
||||
textFieldSuffix?: string;
|
||||
selectOptions: { value: keyof IntervalLimit; label: string }[];
|
||||
hasDeleteButton?: boolean;
|
||||
onDelete: (intervalLimitsKey: IntervalLimitsKey) => void;
|
||||
onLimitChange: (intervalLimitsKey: IntervalLimitsKey, limit: number) => void;
|
||||
onIntervalSelect: (interval: SingleValue<{ value: keyof IntervalLimit; label: string }>) => void;
|
||||
};
|
||||
|
||||
const IntervalLimitItem = ({
|
||||
limitKey,
|
||||
step,
|
||||
value,
|
||||
textFieldSuffix,
|
||||
selectOptions,
|
||||
hasDeleteButton,
|
||||
onDelete,
|
||||
onLimitChange,
|
||||
onIntervalSelect,
|
||||
}: IntervalLimitItemProps) => {
|
||||
return (
|
||||
<div className="mb-2 flex items-center space-x-2 text-sm rtl:space-x-reverse" key={limitKey}>
|
||||
<TextField
|
||||
required
|
||||
type="number"
|
||||
containerClassName={`${textFieldSuffix ? "w-36" : "w-16"} -mb-1`}
|
||||
placeholder={`${value}`}
|
||||
min={step}
|
||||
step={step}
|
||||
defaultValue={value}
|
||||
addOnSuffix={textFieldSuffix}
|
||||
onChange={(e) => onLimitChange(limitKey, parseInt(e.target.value))}
|
||||
/>
|
||||
<Select
|
||||
options={selectOptions}
|
||||
isSearchable={false}
|
||||
defaultValue={INTERVAL_LIMIT_OPTIONS.find((option) => option.value === limitKey)}
|
||||
onChange={onIntervalSelect}
|
||||
/>
|
||||
{hasDeleteButton && (
|
||||
<Button variant="icon" StartIcon={FiTrash} color="destructive" onClick={() => onDelete(limitKey)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type IntervalLimitsManagerProps<K extends "durationLimits" | "bookingLimits"> = {
|
||||
propertyName: K;
|
||||
defaultLimit: number;
|
||||
step: number;
|
||||
textFieldSuffix?: string;
|
||||
};
|
||||
|
||||
const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||
propertyName,
|
||||
defaultLimit,
|
||||
step,
|
||||
textFieldSuffix,
|
||||
}: IntervalLimitsManagerProps<K>) => {
|
||||
const { watch, setValue, control } = useFormContext<FormValues>();
|
||||
const watchBookingLimits = watch("bookingLimits");
|
||||
const watchIntervalLimits = watch(propertyName);
|
||||
const { t } = useLocale();
|
||||
|
||||
const [animateRef] = useAutoAnimate<HTMLUListElement>();
|
||||
|
||||
const BOOKING_LIMIT_OPTIONS: {
|
||||
value: keyof BookingLimit;
|
||||
label: string;
|
||||
}[] = [
|
||||
{
|
||||
value: "PER_DAY",
|
||||
label: "Per Day",
|
||||
},
|
||||
{
|
||||
value: "PER_WEEK",
|
||||
label: "Per Week",
|
||||
},
|
||||
{
|
||||
value: "PER_MONTH",
|
||||
label: "Per Month",
|
||||
},
|
||||
{
|
||||
value: "PER_YEAR",
|
||||
label: "Per Year",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name="bookingLimits"
|
||||
name={propertyName}
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => {
|
||||
const currentBookingLimits = value;
|
||||
const currentIntervalLimits = value;
|
||||
|
||||
const addLimit = () => {
|
||||
if (!currentIntervalLimits || !watchIntervalLimits) return;
|
||||
const currentKeys = Object.keys(watchIntervalLimits);
|
||||
|
||||
const [rest] = Object.values(INTERVAL_LIMIT_OPTIONS).filter(
|
||||
(option) => !currentKeys.includes(option.value)
|
||||
);
|
||||
if (!rest || !currentKeys.length) return;
|
||||
//currentDurationLimits is always defined so can be casted
|
||||
// @ts-expect-error FIXME Fix these typings
|
||||
setValue(propertyName, {
|
||||
...watchIntervalLimits,
|
||||
[rest.value]: defaultLimit,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ul ref={animateRef}>
|
||||
{currentBookingLimits &&
|
||||
watchBookingLimits &&
|
||||
Object.entries(currentBookingLimits)
|
||||
.sort(([limitkeyA], [limitKeyB]) => {
|
||||
{currentIntervalLimits &&
|
||||
watchIntervalLimits &&
|
||||
Object.entries(currentIntervalLimits)
|
||||
.sort(([limitKeyA], [limitKeyB]) => {
|
||||
return (
|
||||
validationOrderKeys.indexOf(limitkeyA as BookingLimitsKey) -
|
||||
validationOrderKeys.indexOf(limitKeyB as BookingLimitsKey)
|
||||
intervalOrderKeys.indexOf(limitKeyA as IntervalLimitsKey) -
|
||||
intervalOrderKeys.indexOf(limitKeyB as IntervalLimitsKey)
|
||||
);
|
||||
})
|
||||
.map(([key, bookingAmount]) => {
|
||||
const bookingLimitKey = key as BookingLimitsKey;
|
||||
.map(([key, value]) => {
|
||||
const limitKey = key as IntervalLimitsKey;
|
||||
return (
|
||||
<div
|
||||
className="mb-2 flex items-center space-x-2 text-sm rtl:space-x-reverse"
|
||||
key={bookingLimitKey}>
|
||||
<Input
|
||||
id={`${bookingLimitKey}-limit`}
|
||||
type="number"
|
||||
className="mb-0 block w-16 rounded-md border-gray-300 text-sm [appearance:textfield]"
|
||||
placeholder="1"
|
||||
min={1}
|
||||
defaultValue={bookingAmount}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setValue(`bookingLimits.${bookingLimitKey}`, parseInt(val));
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
options={BOOKING_LIMIT_OPTIONS.filter(
|
||||
(option) => !Object.keys(currentBookingLimits).includes(option.value)
|
||||
)}
|
||||
isSearchable={false}
|
||||
defaultValue={BOOKING_LIMIT_OPTIONS.find((option) => option.value === key)}
|
||||
onChange={(val) => {
|
||||
const current = currentBookingLimits;
|
||||
const currentValue = watchBookingLimits[bookingLimitKey];
|
||||
<IntervalLimitItem
|
||||
key={key}
|
||||
limitKey={limitKey}
|
||||
step={step}
|
||||
value={value}
|
||||
textFieldSuffix={textFieldSuffix}
|
||||
hasDeleteButton={Object.keys(currentIntervalLimits).length > 1}
|
||||
selectOptions={INTERVAL_LIMIT_OPTIONS.filter(
|
||||
(option) => !Object.keys(currentIntervalLimits).includes(option.value)
|
||||
)}
|
||||
onLimitChange={(intervalLimitKey, val) =>
|
||||
// @ts-expect-error FIXME Fix these typings
|
||||
setValue(`${propertyName}.${intervalLimitKey}`, val)
|
||||
}
|
||||
onDelete={(intervalLimitKey) => {
|
||||
const current = currentIntervalLimits;
|
||||
delete current[intervalLimitKey];
|
||||
onChange(current);
|
||||
}}
|
||||
onIntervalSelect={(interval) => {
|
||||
const current = currentIntervalLimits;
|
||||
const currentValue = watchIntervalLimits[limitKey];
|
||||
|
||||
// Removes limit from previous selected value (eg when changed from per_week to per_month, we unset per_week here)
|
||||
delete current[bookingLimitKey];
|
||||
const newData = {
|
||||
...current,
|
||||
// Set limit to new selected value (in the example above this means we set the limit to per_week here).
|
||||
[val?.value as BookingLimitsKey]: currentValue,
|
||||
};
|
||||
onChange(newData);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="icon"
|
||||
StartIcon={FiTrash}
|
||||
color="destructive"
|
||||
onClick={() => {
|
||||
const current = currentBookingLimits;
|
||||
delete current[key as BookingLimitsKey];
|
||||
onChange(current);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
// Removes limit from previous selected value (eg when changed from per_week to per_month, we unset per_week here)
|
||||
delete current[limitKey];
|
||||
const newData = {
|
||||
...current,
|
||||
// Set limit to new selected value (in the example above this means we set the limit to per_week here).
|
||||
[interval?.value as IntervalLimitsKey]: currentValue,
|
||||
};
|
||||
onChange(newData);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{currentBookingLimits && Object.keys(currentBookingLimits).length <= 3 && (
|
||||
<Button
|
||||
color="minimal"
|
||||
StartIcon={FiPlus}
|
||||
onClick={() => {
|
||||
if (!currentBookingLimits || !watchBookingLimits) return;
|
||||
const currentKeys = Object.keys(watchBookingLimits);
|
||||
|
||||
const rest = Object.values(BOOKING_LIMIT_OPTIONS).filter(
|
||||
(option) => !currentKeys.includes(option.value)
|
||||
);
|
||||
if (!rest || !currentKeys) return;
|
||||
//currentBookingLimits is always defined so can be casted
|
||||
|
||||
setValue("bookingLimits", {
|
||||
...watchBookingLimits,
|
||||
[rest[0].value]: 1,
|
||||
});
|
||||
}}>
|
||||
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && (
|
||||
<Button color="minimal" StartIcon={FiPlus} onClick={addLimit}>
|
||||
{t("add_limit")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import type { SchedulingType } from "@prisma/client";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { ComponentProps } from "react";
|
||||
import type { Control } from "react-hook-form";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import type { Options } from "react-select";
|
||||
|
||||
@@ -35,65 +35,169 @@ const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof
|
||||
return 0;
|
||||
};
|
||||
|
||||
const FixedHosts = ({
|
||||
control,
|
||||
const CheckedHostField = ({
|
||||
labelText,
|
||||
placeholder,
|
||||
options = [],
|
||||
isFixed,
|
||||
value,
|
||||
onChange,
|
||||
...rest
|
||||
}: {
|
||||
control: Control<FormValues>;
|
||||
labelText: string;
|
||||
placeholder: string;
|
||||
isFixed: boolean;
|
||||
value: { isFixed: boolean; userId: number }[];
|
||||
onChange?: (options: { isFixed: boolean; userId: number }[]) => void;
|
||||
options?: Options<CheckedSelectOption>;
|
||||
} & Partial<ComponentProps<typeof CheckedTeamSelect>>) => {
|
||||
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
|
||||
<div>
|
||||
<Label>{labelText}</Label>
|
||||
<Controller
|
||||
name="hostsFixed"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<CheckedTeamSelect
|
||||
isDisabled={false}
|
||||
onChange={(options) => {
|
||||
onChange(
|
||||
options.map((option) => ({
|
||||
isFixed: true,
|
||||
userId: parseInt(option.value, 10),
|
||||
}))
|
||||
);
|
||||
}}
|
||||
value={value
|
||||
.map(
|
||||
(host) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
options.find((member) => member.value === host.userId.toString())!
|
||||
)
|
||||
.filter(Boolean)}
|
||||
controlShouldRenderValue={false}
|
||||
options={options}
|
||||
placeholder={placeholder}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
<CheckedTeamSelect
|
||||
isOptionDisabled={(option) => !!value.find((host) => host.userId.toString() === option.value)}
|
||||
onChange={(options) => {
|
||||
onChange &&
|
||||
onChange(
|
||||
options.map((option) => ({
|
||||
isFixed,
|
||||
userId: parseInt(option.value, 10),
|
||||
}))
|
||||
);
|
||||
}}
|
||||
value={(value || [])
|
||||
.filter(({ isFixed: _isFixed }) => isFixed === _isFixed)
|
||||
.map(
|
||||
(host) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
options.find((member) => member.value === host.userId.toString())!
|
||||
)
|
||||
.filter(Boolean)}
|
||||
controlShouldRenderValue={false}
|
||||
options={options}
|
||||
placeholder={placeholder}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const RoundRobinHosts = ({
|
||||
teamMembers,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: { isFixed: boolean; userId: number }[];
|
||||
onChange: (hosts: { isFixed: boolean; userId: number }[]) => void;
|
||||
teamMembers: {
|
||||
value: string;
|
||||
label: string;
|
||||
avatar: string;
|
||||
email: string;
|
||||
}[];
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<>
|
||||
<CheckedHostField
|
||||
options={teamMembers.sort(sortByLabel)}
|
||||
isFixed={true}
|
||||
onChange={(changeValue) => {
|
||||
onChange([...value.filter(({ isFixed }) => !isFixed), ...changeValue]);
|
||||
}}
|
||||
value={value}
|
||||
placeholder={t("add_fixed_hosts")}
|
||||
labelText={t("fixed_hosts")}
|
||||
/>
|
||||
<CheckedHostField
|
||||
options={teamMembers.sort(sortByLabel)}
|
||||
onChange={(changeValue) => onChange([...value.filter(({ isFixed }) => isFixed), ...changeValue])}
|
||||
value={value}
|
||||
isFixed={false}
|
||||
placeholder={t("add_attendees")}
|
||||
labelText={t("round_robin_hosts")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Hosts = ({
|
||||
teamMembers,
|
||||
}: {
|
||||
teamMembers: {
|
||||
value: string;
|
||||
label: string;
|
||||
avatar: string;
|
||||
email: string;
|
||||
}[];
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
control,
|
||||
resetField,
|
||||
getValues,
|
||||
formState: { submitCount },
|
||||
} = useFormContext<FormValues>();
|
||||
const schedulingType = useWatch({
|
||||
control: formMethods.control,
|
||||
control,
|
||||
name: "schedulingType",
|
||||
});
|
||||
const initialValue = useRef<{
|
||||
hosts: FormValues["hosts"];
|
||||
schedulingType: SchedulingType | null;
|
||||
submitCount: number;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Handles init & out of date initial value after submission.
|
||||
if (!initialValue.current || initialValue.current?.submitCount !== submitCount) {
|
||||
initialValue.current = { hosts: getValues("hosts"), schedulingType, submitCount };
|
||||
return;
|
||||
}
|
||||
resetField("hosts", {
|
||||
defaultValue: initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
|
||||
});
|
||||
}, [schedulingType, resetField, getValues, submitCount]);
|
||||
|
||||
return (
|
||||
<Controller<FormValues>
|
||||
name="hosts"
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const schedulingTypeRender = {
|
||||
COLLECTIVE: (
|
||||
<CheckedHostField
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isFixed={true}
|
||||
options={teamMembers.sort(sortByLabel)}
|
||||
placeholder={t("add_attendees")}
|
||||
labelText={t("team")}
|
||||
/>
|
||||
),
|
||||
ROUND_ROBIN: (
|
||||
<>
|
||||
<RoundRobinHosts teamMembers={teamMembers} onChange={onChange} value={value} />
|
||||
{/*<TextField
|
||||
required
|
||||
type="number"
|
||||
label={t("minimum_round_robin_hosts_count")}
|
||||
defaultValue={1}
|
||||
{...formMethods.register("minimumHostCount")}
|
||||
addOnSuffix={<>{t("hosts")}</>}
|
||||
/>*/}
|
||||
</>
|
||||
),
|
||||
};
|
||||
return !!schedulingType ? schedulingTypeRender[schedulingType] : <></>;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const schedulingTypeOptions: {
|
||||
value: SchedulingType;
|
||||
@@ -101,17 +205,16 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
||||
// description: string;
|
||||
}[] = [
|
||||
{
|
||||
value: SchedulingType.COLLECTIVE,
|
||||
value: "COLLECTIVE",
|
||||
label: t("collective"),
|
||||
// description: t("collective_description"),
|
||||
},
|
||||
{
|
||||
value: SchedulingType.ROUND_ROBIN,
|
||||
value: "ROUND_ROBIN",
|
||||
label: t("round_robin"),
|
||||
// description: t("round_robin_description"),
|
||||
},
|
||||
];
|
||||
|
||||
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
||||
return (
|
||||
<div>
|
||||
@@ -119,9 +222,8 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
||||
<div className="space-y-5">
|
||||
<div className="flex flex-col">
|
||||
<Label>{t("scheduling_type")}</Label>
|
||||
<Controller
|
||||
<Controller<FormValues>
|
||||
name="schedulingType"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Select
|
||||
options={schedulingTypeOptions}
|
||||
@@ -134,75 +236,7 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{schedulingType === SchedulingType.COLLECTIVE && (
|
||||
<FixedHosts
|
||||
options={teamMembersOptions.sort(sortByLabel)}
|
||||
placeholder={t("add_attendees")}
|
||||
labelText={t("team")}
|
||||
control={formMethods.control}
|
||||
/>
|
||||
)}
|
||||
{schedulingType === SchedulingType.ROUND_ROBIN && (
|
||||
<>
|
||||
<FixedHosts
|
||||
options={teamMembersOptions.sort(sortByLabel)}
|
||||
isOptionDisabled={(option) =>
|
||||
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
|
||||
}
|
||||
placeholder={t("add_fixed_hosts")}
|
||||
labelText={t("fixed_hosts")}
|
||||
control={formMethods.control}
|
||||
/>
|
||||
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
|
||||
<div>
|
||||
<Label>{t("round_robin_hosts")}</Label>
|
||||
<Controller
|
||||
name="hosts"
|
||||
control={formMethods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CheckedTeamSelect
|
||||
isDisabled={false}
|
||||
onChange={(options) =>
|
||||
onChange(
|
||||
options.map((option) => ({
|
||||
isFixed: false,
|
||||
userId: parseInt(option.value, 10),
|
||||
}))
|
||||
)
|
||||
}
|
||||
value={value
|
||||
.map(
|
||||
(host) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
teamMembers
|
||||
.map(mapUserToValue)
|
||||
.find((member) => member.value === host.userId.toString())!
|
||||
)
|
||||
.filter(Boolean)}
|
||||
controlShouldRenderValue={false}
|
||||
options={teamMembersOptions.sort(sortByLabel)}
|
||||
isOptionDisabled={(option) =>
|
||||
!!formMethods
|
||||
.getValues("hostsFixed")
|
||||
.find((host) => host.userId.toString() === option.value)
|
||||
}
|
||||
placeholder={t("add_attendees")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/*<TextField
|
||||
required
|
||||
type="number"
|
||||
label={t("minimum_round_robin_hosts_count")}
|
||||
defaultValue={1}
|
||||
{...formMethods.register("minimumHostCount")}
|
||||
addOnSuffix={<>{t("hosts")}</>}
|
||||
/>*/}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Hosts teamMembers={teamMembersOptions} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { SyntheticEvent } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { ErrorCode } from "@calcom/lib/auth";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/lib/auth";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { BaseSyntheticEvent } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/lib/auth";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Dialog, DialogContent, Form } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/lib/auth";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { BaseSyntheticEvent } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/lib/auth";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Dialog, DialogContent, Form, TextField } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { isPasswordValid } from "@calcom/lib/auth";
|
||||
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import Link from "next/link";
|
||||
import type { TeamPageProps } from "pages/team/[slug]";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
|
||||
type TeamType = TeamPageProps["team"];
|
||||
type TeamType = NonNullable<TeamWithMembers>;
|
||||
type MembersType = TeamType["members"];
|
||||
type MemberType = MembersType[number];
|
||||
|
||||
@@ -57,7 +57,7 @@ const Members = ({ members, teamName }: { members: MembersType; teamName: string
|
||||
);
|
||||
};
|
||||
|
||||
const Team = ({ team }: Omit<TeamPageProps, "trpcState">) => {
|
||||
const Team = ({ team }: { team: TeamType }) => {
|
||||
return (
|
||||
<div>
|
||||
<Members members={team.members} teamName={team.name} />
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import { compare, hash } from "bcryptjs";
|
||||
import type { Session } from "next-auth";
|
||||
import type { GetSessionParams } from "next-auth/react";
|
||||
import { getSession as getSessionInner } from "next-auth/react";
|
||||
|
||||
/** @deprecated use the one from `@calcom/lib/auth` */
|
||||
export async function hashPassword(password: string) {
|
||||
const hashedPassword = await hash(password, 12);
|
||||
return hashedPassword;
|
||||
}
|
||||
/** @deprecated use the one from `@calcom/lib/auth` */
|
||||
export async function verifyPassword(password: string, hashedPassword: string) {
|
||||
const isValid = await compare(password, hashedPassword);
|
||||
return isValid;
|
||||
}
|
||||
/** @deprecated use the one from `@calcom/lib/auth` */
|
||||
export async function getSession(options: GetSessionParams): Promise<Session | null> {
|
||||
const session = await getSessionInner(options);
|
||||
|
||||
// that these are equal are ensured in `[...nextauth]`'s callback
|
||||
return session as Session | null;
|
||||
}
|
||||
/** @deprecated use the one from `@calcom/lib/auth` */
|
||||
export enum ErrorCode {
|
||||
UserNotFound = "user-not-found",
|
||||
IncorrectPassword = "incorrect-password",
|
||||
UserMissingPassword = "missing-password",
|
||||
TwoFactorDisabled = "two-factor-disabled",
|
||||
TwoFactorAlreadyEnabled = "two-factor-already-enabled",
|
||||
TwoFactorSetupRequired = "two-factor-setup-required",
|
||||
SecondFactorRequired = "second-factor-required",
|
||||
IncorrectTwoFactorCode = "incorrect-two-factor-code",
|
||||
InternalServerError = "internal-server-error",
|
||||
NewPasswordMatchesOld = "new-password-matches-old",
|
||||
ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled",
|
||||
RateLimitExceeded = "rate-limit-exceeded",
|
||||
InvalidPassword = "invalid-password",
|
||||
}
|
||||
/** @deprecated use the one from `@calcom/lib/auth` */
|
||||
export const identityProviderNameMap: { [key in IdentityProvider]: string } = {
|
||||
[IdentityProvider.CAL]: "Cal",
|
||||
[IdentityProvider.GOOGLE]: "Google",
|
||||
[IdentityProvider.SAML]: "SAML",
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import parser from "accept-language-parser";
|
||||
import type { IncomingMessage } from "http";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import type { Maybe } from "@calcom/trpc/server";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
import { i18n } from "../../../next-i18next.config";
|
||||
|
||||
export function getLocaleFromHeaders(req: IncomingMessage): string {
|
||||
@@ -15,9 +15,13 @@ export function getLocaleFromHeaders(req: IncomingMessage): string {
|
||||
return preferredLocale ?? i18n.defaultLocale;
|
||||
}
|
||||
|
||||
export const getOrSetUserLocaleFromHeaders = async (req: IncomingMessage): Promise<string> => {
|
||||
const prisma = (await import("@calcom/prisma")).default;
|
||||
const session = await getSession({ req });
|
||||
export const getOrSetUserLocaleFromHeaders = async (
|
||||
req: GetServerSidePropsContext["req"],
|
||||
res: GetServerSidePropsContext["res"]
|
||||
): Promise<string> => {
|
||||
const { default: prisma } = await import("@calcom/prisma");
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
const preferredLocale = getLocaleFromHeaders(req);
|
||||
|
||||
if (session?.user?.id) {
|
||||
|
||||
@@ -55,6 +55,8 @@ async function getBooking(
|
||||
uid,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
uid: true,
|
||||
startTime: true,
|
||||
description: true,
|
||||
customInputs: true,
|
||||
@@ -65,6 +67,12 @@ async function getBooking(
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
bookingSeat: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,21 +3,23 @@ import { RRule } from "rrule";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import { detectBrowserTimeFormat, TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { parseZone } from "./parseZone";
|
||||
|
||||
const processDate = (date: string | null | Dayjs, i18n: I18n) => {
|
||||
const processDate = (date: string | null | Dayjs, i18n: I18n, withDefaultTimeFormat: boolean) => {
|
||||
const parsedZone = parseZone(date);
|
||||
if (!parsedZone?.isValid()) return "Invalid date";
|
||||
const formattedTime = parsedZone?.format(detectBrowserTimeFormat);
|
||||
const formattedTime = parsedZone?.format(
|
||||
withDefaultTimeFormat ? TimeFormat.TWELVE_HOUR : detectBrowserTimeFormat
|
||||
);
|
||||
return formattedTime + ", " + dayjs(date).toDate().toLocaleString(i18n.language, { dateStyle: "full" });
|
||||
};
|
||||
|
||||
export const parseDate = (date: string | null | Dayjs, i18n: I18n) => {
|
||||
export const parseDate = (date: string | null | Dayjs, i18n: I18n, withDefaultTimeFormat: boolean) => {
|
||||
if (!date) return ["No date"];
|
||||
return processDate(date, i18n);
|
||||
return processDate(date, i18n, withDefaultTimeFormat);
|
||||
};
|
||||
|
||||
export const parseRecurringDates = (
|
||||
@@ -26,11 +28,13 @@ export const parseRecurringDates = (
|
||||
timeZone,
|
||||
recurringEvent,
|
||||
recurringCount,
|
||||
withDefaultTimeFormat,
|
||||
}: {
|
||||
startDate: string | null | Dayjs;
|
||||
timeZone?: string;
|
||||
recurringEvent: RecurringEvent | null;
|
||||
recurringCount: number;
|
||||
withDefaultTimeFormat: boolean;
|
||||
},
|
||||
i18n: I18n
|
||||
): [string[], Date[]] => {
|
||||
@@ -50,7 +54,7 @@ export const parseRecurringDates = (
|
||||
});
|
||||
const dateStrings = times.map((t) => {
|
||||
// finally; show in local timeZone again
|
||||
return processDate(t.tz(timeZone), i18n);
|
||||
return processDate(t.tz(timeZone), i18n, withDefaultTimeFormat);
|
||||
});
|
||||
|
||||
return [dateStrings, times.map((t) => t.toDate())];
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import type { Attendee, Booking } from "@prisma/client";
|
||||
|
||||
import type { AppsStatus } from "@calcom/types/Calendar";
|
||||
|
||||
export type BookingResponse = Booking & {
|
||||
paymentUid?: string;
|
||||
attendees: Attendee[];
|
||||
appsStatus?: AppsStatus[];
|
||||
};
|
||||
export type BookingResponse = Awaited<
|
||||
ReturnType<typeof import("@calcom/features/bookings/lib/handleNewBooking").default>
|
||||
>;
|
||||
|
||||
+10
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "2.6.8",
|
||||
"version": "2.7.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -19,18 +19,18 @@
|
||||
"check-changed-files": "ts-node scripts/ts-check-changed-files.ts"
|
||||
},
|
||||
"engines": {
|
||||
"node": "16.x || 18.x",
|
||||
"yarn": ">=1.19.0 < 2.0.0"
|
||||
"node": "18",
|
||||
"yarn": "3.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@boxyhq/saml-jackson": "1.7.1",
|
||||
"@boxyhq/saml-jackson": "1.8.2",
|
||||
"@calcom/app-store": "*",
|
||||
"@calcom/app-store-cli": "*",
|
||||
"@calcom/core": "*",
|
||||
"@calcom/dayjs": "*",
|
||||
"@calcom/embed-core": "*",
|
||||
"@calcom/embed-react": "*",
|
||||
"@calcom/embed-snippet": "*",
|
||||
"@calcom/embed-core": "workspace:*",
|
||||
"@calcom/embed-react": "workspace:*",
|
||||
"@calcom/embed-snippet": "workspace:*",
|
||||
"@calcom/features": "*",
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*",
|
||||
@@ -69,6 +69,7 @@
|
||||
"bcryptjs": "^2.4.3",
|
||||
"classnames": "^2.3.1",
|
||||
"dotenv-cli": "^6.0.0",
|
||||
"entities": "^4.4.0",
|
||||
"eslint-config-next": "^13.2.1",
|
||||
"googleapis": "^84.0.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
@@ -86,7 +87,7 @@
|
||||
"micro": "^10.0.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"next": "^13.2.1",
|
||||
"next-auth": "^4.18.8",
|
||||
"next-auth": "^4.20.1",
|
||||
"next-axiom": "^0.16.0",
|
||||
"next-collect": "^0.2.1",
|
||||
"next-i18next": "^11.3.0",
|
||||
@@ -133,7 +134,7 @@
|
||||
"@calcom/config": "*",
|
||||
"@calcom/types": "*",
|
||||
"@microsoft/microsoft-graph-types-beta": "0.15.0-preview",
|
||||
"@playwright/test": "^1.25.0",
|
||||
"@playwright/test": "^1.31.2",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@types/accept-language-parser": "1.5.2",
|
||||
"@types/async": "^3.2.15",
|
||||
@@ -161,7 +162,6 @@
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"detect-port": "^1.3.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"eslint": "^8.34.0",
|
||||
"mockdate": "^3.0.5",
|
||||
"module-alias": "^2.2.2",
|
||||
"msw": "^0.42.3",
|
||||
|
||||
+30
-23
@@ -15,6 +15,7 @@ import {
|
||||
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import defaultEvents, {
|
||||
getDynamicEventDescription,
|
||||
getGroupName,
|
||||
@@ -76,7 +77,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
size="sm"
|
||||
items={props.users.map((user) => ({
|
||||
alt: user.name || "",
|
||||
image: user.avatar || "",
|
||||
image: user.avatar,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
@@ -177,26 +178,28 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
className="dark:bg-darkgray-100 dark:hover:bg-darkgray-200 dark:border-darkgray-300 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50">
|
||||
<FiArrowRight className="absolute right-4 top-4 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
|
||||
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={{
|
||||
pathname: `/${user.username}/${type.slug}`,
|
||||
query,
|
||||
}}
|
||||
onClick={async () => {
|
||||
sdkActionManager?.fire("eventTypeSelected", {
|
||||
eventType: type,
|
||||
});
|
||||
}}
|
||||
className="block w-full p-5"
|
||||
data-testid="event-type-link">
|
||||
<div className="flex flex-wrap items-center">
|
||||
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
|
||||
{type.title}
|
||||
</h2>
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} />
|
||||
</Link>
|
||||
<div className="block w-full p-5">
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={{
|
||||
pathname: `/${user.username}/${type.slug}`,
|
||||
query,
|
||||
}}
|
||||
passHref
|
||||
onClick={async () => {
|
||||
sdkActionManager?.fire("eventTypeSelected", {
|
||||
eventType: type,
|
||||
});
|
||||
}}
|
||||
data-testid="event-type-link">
|
||||
<div className="flex flex-wrap items-center">
|
||||
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
|
||||
{type.title}
|
||||
</h2>
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@@ -259,7 +262,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
const usernameList = getUsernameList(context.query.user as string);
|
||||
const dataFetchStart = Date.now();
|
||||
const users = await prisma.user.findMany({
|
||||
const usersWithoutAvatar = await prisma.user.findMany({
|
||||
where: {
|
||||
username: {
|
||||
in: usernameList,
|
||||
@@ -273,7 +276,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
bio: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
avatar: true,
|
||||
theme: true,
|
||||
away: true,
|
||||
verified: true,
|
||||
@@ -281,6 +283,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
},
|
||||
});
|
||||
|
||||
const users = usersWithoutAvatar.map((user) => ({
|
||||
...user,
|
||||
avatar: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
||||
}));
|
||||
|
||||
if (!users.length) {
|
||||
return {
|
||||
notFound: true,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import { privacyFilteredLocations } from "@calcom/app-store/locations";
|
||||
import { getAppFromSlug } from "@calcom/app-store/utils";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import {
|
||||
@@ -12,15 +14,13 @@ import {
|
||||
getUsernameList,
|
||||
} from "@calcom/lib/defaultEvents";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { bookEventTypeSelect } from "@calcom/prisma";
|
||||
import prisma from "@calcom/prisma";
|
||||
import prisma, { bookEventTypeSelect } from "@calcom/prisma";
|
||||
import {
|
||||
customInputSchema,
|
||||
EventTypeMetaDataSchema,
|
||||
userMetadata as userMetadataSchema,
|
||||
} from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
||||
import type { GetBookingType } from "@lib/getBooking";
|
||||
import getBooking from "@lib/getBooking";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
@@ -70,11 +70,26 @@ export default function Book(props: BookPageProps) {
|
||||
|
||||
Book.isThemeSupported = true;
|
||||
|
||||
const querySchema = z.object({
|
||||
bookingUid: z.string().optional(),
|
||||
count: z.coerce.number().optional(),
|
||||
embed: z.string().optional(),
|
||||
rescheduleUid: z.string().optional(),
|
||||
slug: z.string().optional(),
|
||||
/** This is the event "type" ID */
|
||||
type: z.coerce.number().optional(),
|
||||
user: z.string(),
|
||||
seatReferenceUid: z.string().optional(),
|
||||
date: z.string().optional(),
|
||||
duration: z.coerce.number().optional(),
|
||||
});
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const ssr = await ssrInit(context);
|
||||
const usernameList = getUsernameList(asStringOrThrow(context.query.user as string));
|
||||
const eventTypeSlug = context.query.slug as string;
|
||||
const recurringEventCountQuery = asStringOrNull(context.query.count);
|
||||
const query = querySchema.parse(context.query);
|
||||
const usernameList = getUsernameList(query.user);
|
||||
const eventTypeSlug = query.slug;
|
||||
const recurringEventCountQuery = query.count;
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
username: {
|
||||
@@ -99,24 +114,23 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
|
||||
if (!users.length) return { notFound: true };
|
||||
const [user] = users;
|
||||
const isDynamicGroupBooking = users.length > 1;
|
||||
const isDynamicGroupBooking = users.length > 1 && !!eventTypeSlug;
|
||||
|
||||
// Dynamic Group link doesn't need a type but it must have a slug
|
||||
if ((!isDynamicGroupBooking && !context.query.type) || (isDynamicGroupBooking && !eventTypeSlug)) {
|
||||
if ((!isDynamicGroupBooking && !query.type) || (users.length > 1 && !eventTypeSlug)) {
|
||||
return { notFound: true };
|
||||
}
|
||||
|
||||
const eventTypeRaw =
|
||||
usernameList.length > 1
|
||||
? getDefaultEvent(eventTypeSlug)
|
||||
: await prisma.eventType.findUnique({
|
||||
where: {
|
||||
id: parseInt(asStringOrThrow(context.query.type)),
|
||||
},
|
||||
select: {
|
||||
...bookEventTypeSelect,
|
||||
},
|
||||
});
|
||||
const eventTypeRaw = isDynamicGroupBooking
|
||||
? getDefaultEvent(eventTypeSlug)
|
||||
: await prisma.eventType.findUnique({
|
||||
where: {
|
||||
id: query.type,
|
||||
},
|
||||
select: {
|
||||
...bookEventTypeSelect,
|
||||
},
|
||||
});
|
||||
|
||||
if (!eventTypeRaw) return { notFound: true };
|
||||
const eventType = {
|
||||
@@ -178,22 +192,68 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
};
|
||||
})[0];
|
||||
|
||||
// If rescheduleUid and event has seats lets convert Uid to bookingUid
|
||||
let rescheduleUid = query.rescheduleUid;
|
||||
const rescheduleEventTypeHasSeats = query.rescheduleUid && eventTypeRaw.seatsPerTimeSlot;
|
||||
let attendeeEmail: string;
|
||||
let bookingUidWithSeats: string | null = null;
|
||||
|
||||
if (rescheduleEventTypeHasSeats) {
|
||||
const bookingSeat = await prisma.bookingSeat.findFirst({
|
||||
where: {
|
||||
referenceUid: query.rescheduleUid,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
attendee: true,
|
||||
booking: {
|
||||
select: {
|
||||
uid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (bookingSeat) {
|
||||
rescheduleUid = bookingSeat.booking.uid;
|
||||
attendeeEmail = bookingSeat.attendee.email;
|
||||
}
|
||||
} else if (eventTypeRaw.seatsPerTimeSlot && query.duration && query.date) {
|
||||
// If it's not reschedule but event Type has seats we should obtain
|
||||
// the bookingUid regardless and use it to get the booking
|
||||
const currentSeats = await prisma.booking.findFirst({
|
||||
where: {
|
||||
eventTypeId: eventTypeRaw.id,
|
||||
startTime: dayjs(query.date).toISOString(),
|
||||
endTime: dayjs(query.date).add(query.duration, "minutes").toISOString(),
|
||||
},
|
||||
select: {
|
||||
uid: true,
|
||||
},
|
||||
});
|
||||
if (currentSeats && currentSeats) {
|
||||
bookingUidWithSeats = currentSeats.uid;
|
||||
}
|
||||
}
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (context.query.rescheduleUid || context.query.bookingUid) {
|
||||
if (rescheduleUid || query.bookingUid || bookingUidWithSeats) {
|
||||
booking = await getBooking(
|
||||
prisma,
|
||||
context.query.rescheduleUid
|
||||
? (context.query.rescheduleUid as string)
|
||||
: (context.query.bookingUid as string),
|
||||
rescheduleUid || query.bookingUid || bookingUidWithSeats || "",
|
||||
eventTypeObject.bookingFields
|
||||
);
|
||||
}
|
||||
|
||||
const dynamicNames = isDynamicGroupBooking
|
||||
? users.map((user) => {
|
||||
return user.name || "";
|
||||
})
|
||||
: [];
|
||||
if (rescheduleEventTypeHasSeats && booking?.attendees && booking?.attendees.length > 0) {
|
||||
const currentAttendee = booking?.attendees.find((attendee) => {
|
||||
return attendee.email === attendeeEmail;
|
||||
});
|
||||
if (currentAttendee) {
|
||||
booking.attendees = [currentAttendee] || [];
|
||||
}
|
||||
}
|
||||
|
||||
const dynamicNames = isDynamicGroupBooking ? users.map((user) => user.name || "") : [];
|
||||
|
||||
const profile = isDynamicGroupBooking
|
||||
? {
|
||||
@@ -203,9 +263,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
theme: null,
|
||||
brandColor: "",
|
||||
darkBrandColor: "",
|
||||
allowDynamicBooking: !users.some((user) => {
|
||||
return !user.allowDynamicBooking;
|
||||
}),
|
||||
allowDynamicBooking: !users.some((user) => !user.allowDynamicBooking),
|
||||
eventName: getDynamicEventName(dynamicNames, eventTypeSlug),
|
||||
}
|
||||
: {
|
||||
@@ -222,8 +280,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const recurringEventCount =
|
||||
(eventType.recurringEvent?.count &&
|
||||
recurringEventCountQuery &&
|
||||
(parseInt(recurringEventCountQuery) <= eventType.recurringEvent.count
|
||||
? parseInt(recurringEventCountQuery)
|
||||
(recurringEventCountQuery <= eventType.recurringEvent.count
|
||||
? recurringEventCountQuery
|
||||
: eventType.recurringEvent.count)) ||
|
||||
null;
|
||||
|
||||
@@ -238,7 +296,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
isDynamicGroupBooking,
|
||||
hasHashedBookingLink: false,
|
||||
hashedLink: null,
|
||||
isEmbed: typeof context.query.embed === "string",
|
||||
isEmbed: !!query.embed,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,659 +1,5 @@
|
||||
import type { UserPermissionRole } from "@prisma/client";
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import { readFileSync } from "fs";
|
||||
import Handlebars from "handlebars";
|
||||
import { SignJWT } from "jose";
|
||||
import type { Session } from "next-auth";
|
||||
import NextAuth from "next-auth";
|
||||
import { encode } from "next-auth/jwt";
|
||||
import type { Provider } from "next-auth/providers";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import EmailProvider from "next-auth/providers/email";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
import type { TransportOptions } from "nodemailer";
|
||||
import nodemailer from "nodemailer";
|
||||
import { authenticator } from "otplib";
|
||||
import path from "path";
|
||||
|
||||
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
||||
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
|
||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
import { hostedCal, isSAMLLoginEnabled, clientSecretVerifier } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
|
||||
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { defaultCookies } from "@calcom/lib/default-cookies";
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
import rateLimit from "@calcom/lib/rateLimit";
|
||||
import { serverConfig } from "@calcom/lib/serverConfig";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
|
||||
import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options";
|
||||
|
||||
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
|
||||
|
||||
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
|
||||
|
||||
const transporter = nodemailer.createTransport<TransportOptions>({
|
||||
...(serverConfig.transport as TransportOptions),
|
||||
} as TransportOptions);
|
||||
|
||||
const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase();
|
||||
|
||||
const signJwt = async (payload: { email: string }) => {
|
||||
const secret = new TextEncoder().encode(process.env.CALENDSO_ENCRYPTION_KEY);
|
||||
return new SignJWT(payload)
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setSubject(payload.email)
|
||||
.setIssuedAt()
|
||||
.setIssuer(WEBSITE_URL)
|
||||
.setAudience(`${WEBSITE_URL}/auth/login`)
|
||||
.setExpirationTime("2m")
|
||||
.sign(secret);
|
||||
};
|
||||
|
||||
const loginWithTotp = async (user: { email: string }) =>
|
||||
`/auth/login?totp=${await signJwt({ email: user.email })}`;
|
||||
|
||||
const providers: Provider[] = [
|
||||
CredentialsProvider({
|
||||
id: "credentials",
|
||||
name: "Cal.com",
|
||||
type: "credentials",
|
||||
credentials: {
|
||||
email: { label: "Email Address", type: "email", placeholder: "john.doe@example.com" },
|
||||
password: { label: "Password", type: "password", placeholder: "Your super secure password" },
|
||||
totpCode: { label: "Two-factor Code", type: "input", placeholder: "Code from authenticator app" },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials) {
|
||||
console.error(`For some reason credentials are missing`);
|
||||
throw new Error(ErrorCode.InternalServerError);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: credentials.email.toLowerCase(),
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
metadata: true,
|
||||
identityProvider: true,
|
||||
password: true,
|
||||
twoFactorEnabled: true,
|
||||
twoFactorSecret: true,
|
||||
teams: {
|
||||
include: {
|
||||
team: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Don't leak information about it being username or password that is invalid
|
||||
if (!user) {
|
||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||
}
|
||||
|
||||
if (user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
|
||||
throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled);
|
||||
}
|
||||
|
||||
if (!user.password && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
|
||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||
}
|
||||
|
||||
if (user.password) {
|
||||
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
|
||||
if (!isCorrectPassword) {
|
||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||
}
|
||||
}
|
||||
|
||||
if (user.twoFactorEnabled) {
|
||||
if (!credentials.totpCode) {
|
||||
throw new Error(ErrorCode.SecondFactorRequired);
|
||||
}
|
||||
|
||||
if (!user.twoFactorSecret) {
|
||||
console.error(`Two factor is enabled for user ${user.id} but they have no secret`);
|
||||
throw new Error(ErrorCode.InternalServerError);
|
||||
}
|
||||
|
||||
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
|
||||
console.error(`"Missing encryption key; cannot proceed with two factor login."`);
|
||||
throw new Error(ErrorCode.InternalServerError);
|
||||
}
|
||||
|
||||
const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY);
|
||||
if (secret.length !== 32) {
|
||||
console.error(
|
||||
`Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}`
|
||||
);
|
||||
throw new Error(ErrorCode.InternalServerError);
|
||||
}
|
||||
|
||||
const isValidToken = authenticator.check(credentials.totpCode, secret);
|
||||
if (!isValidToken) {
|
||||
throw new Error(ErrorCode.IncorrectTwoFactorCode);
|
||||
}
|
||||
}
|
||||
|
||||
const limiter = rateLimit({
|
||||
intervalInMs: 60 * 1000, // 1 minute
|
||||
});
|
||||
await limiter.check(10, user.email); // 10 requests per minute
|
||||
// Check if the user you are logging into has any active teams
|
||||
const hasActiveTeams =
|
||||
user.teams.filter((m: { team: { metadata: unknown } }) => {
|
||||
if (!IS_TEAM_BILLING_ENABLED) return true;
|
||||
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
|
||||
if (metadata.success && metadata.data?.subscriptionId) return true;
|
||||
return false;
|
||||
}).length > 0;
|
||||
|
||||
// authentication success- but does it meet the minimum password requirements?
|
||||
if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: "INACTIVE_ADMIN",
|
||||
belongsToActiveTeam: hasActiveTeams,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
belongsToActiveTeam: hasActiveTeams,
|
||||
};
|
||||
},
|
||||
}),
|
||||
ImpersonationProvider,
|
||||
];
|
||||
|
||||
if (IS_GOOGLE_LOGIN_ENABLED) {
|
||||
providers.push(
|
||||
GoogleProvider({
|
||||
clientId: GOOGLE_CLIENT_ID,
|
||||
clientSecret: GOOGLE_CLIENT_SECRET,
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isSAMLLoginEnabled) {
|
||||
providers.push({
|
||||
id: "saml",
|
||||
name: "BoxyHQ",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
checks: ["pkce", "state"],
|
||||
authorization: {
|
||||
url: `${WEBAPP_URL}/api/auth/saml/authorize`,
|
||||
params: {
|
||||
scope: "",
|
||||
response_type: "code",
|
||||
provider: "saml",
|
||||
},
|
||||
},
|
||||
token: {
|
||||
url: `${WEBAPP_URL}/api/auth/saml/token`,
|
||||
params: { grant_type: "authorization_code" },
|
||||
},
|
||||
userinfo: `${WEBAPP_URL}/api/auth/saml/userinfo`,
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.id || "",
|
||||
firstName: profile.firstName || "",
|
||||
lastName: profile.lastName || "",
|
||||
email: profile.email || "",
|
||||
name: `${profile.firstName || ""} ${profile.lastName || ""}`.trim(),
|
||||
email_verified: true,
|
||||
};
|
||||
},
|
||||
options: {
|
||||
clientId: "dummy",
|
||||
clientSecret: clientSecretVerifier,
|
||||
},
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
});
|
||||
|
||||
// Idp initiated login
|
||||
providers.push(
|
||||
CredentialsProvider({
|
||||
id: "saml-idp",
|
||||
name: "IdP Login",
|
||||
credentials: {
|
||||
code: {},
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { code } = credentials;
|
||||
|
||||
if (!code) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { oauthController } = await jackson();
|
||||
|
||||
// Fetch access token
|
||||
const { access_token } = await oauthController.token({
|
||||
code,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: `${process.env.NEXTAUTH_URL}`,
|
||||
client_id: "dummy",
|
||||
client_secret: clientSecretVerifier,
|
||||
});
|
||||
|
||||
if (!access_token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fetch user info
|
||||
const userInfo = await oauthController.userInfo(access_token);
|
||||
|
||||
if (!userInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { id, firstName, lastName, email } = userInfo;
|
||||
|
||||
return {
|
||||
id: id as unknown as number,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
name: `${firstName} ${lastName}`.trim(),
|
||||
email_verified: true,
|
||||
};
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (true) {
|
||||
const emailsDir = path.resolve(process.cwd(), "..", "..", "packages/emails", "templates");
|
||||
providers.push(
|
||||
EmailProvider({
|
||||
type: "email",
|
||||
maxAge: 10 * 60 * 60, // Magic links are valid for 10 min only
|
||||
// Here we setup the sendVerificationRequest that calls the email template with the identifier (email) and token to verify.
|
||||
sendVerificationRequest: ({ identifier, url }) => {
|
||||
const originalUrl = new URL(url);
|
||||
const webappUrl = new URL(WEBAPP_URL);
|
||||
if (originalUrl.origin !== webappUrl.origin) {
|
||||
url = url.replace(originalUrl.origin, webappUrl.origin);
|
||||
}
|
||||
const emailFile = readFileSync(path.join(emailsDir, "confirm-email.html"), {
|
||||
encoding: "utf8",
|
||||
});
|
||||
const emailTemplate = Handlebars.compile(emailFile);
|
||||
transporter.sendMail({
|
||||
from: `${process.env.EMAIL_FROM}` || APP_NAME,
|
||||
to: identifier,
|
||||
subject: "Your sign-in link for " + APP_NAME,
|
||||
html: emailTemplate({
|
||||
base_url: WEBAPP_URL,
|
||||
signin_url: url,
|
||||
email: identifier,
|
||||
}),
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function isNumber(n: string) {
|
||||
return !isNaN(parseFloat(n)) && !isNaN(+n);
|
||||
}
|
||||
|
||||
const calcomAdapter = CalComAdapter(prisma);
|
||||
|
||||
export default NextAuth({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
adapter: calcomAdapter,
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
jwt: {
|
||||
// decorate the native JWT encode function
|
||||
// Impl. detail: We don't pass through as this function is called with encode/decode functions.
|
||||
encode: async ({ token, maxAge, secret }) => {
|
||||
if (token?.sub && isNumber(token.sub)) {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { id: Number(token.sub) },
|
||||
select: { metadata: true },
|
||||
});
|
||||
// if no user is found, we still don't want to crash here.
|
||||
if (user) {
|
||||
const metadata = userMetadata.parse(user.metadata);
|
||||
if (metadata?.sessionTimeout) {
|
||||
maxAge = metadata.sessionTimeout / 60;
|
||||
}
|
||||
}
|
||||
}
|
||||
return encode({ secret, token, maxAge });
|
||||
},
|
||||
},
|
||||
cookies: defaultCookies(WEBAPP_URL?.startsWith("https://")),
|
||||
pages: {
|
||||
signIn: "/auth/login",
|
||||
signOut: "/auth/logout",
|
||||
error: "/auth/error", // Error code passed in query string as ?error=
|
||||
verifyRequest: "/auth/verify",
|
||||
// newUser: "/auth/new", // New users will be directed here on first sign in (leave the property out if not of interest)
|
||||
},
|
||||
providers,
|
||||
callbacks: {
|
||||
async jwt({ token, user, account }) {
|
||||
const autoMergeIdentities = async () => {
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
where: { email: token.email! },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingUser) {
|
||||
return token;
|
||||
}
|
||||
|
||||
return {
|
||||
...existingUser,
|
||||
...token,
|
||||
};
|
||||
};
|
||||
if (!user) {
|
||||
return await autoMergeIdentities();
|
||||
}
|
||||
if (!account) {
|
||||
return token;
|
||||
}
|
||||
if (account.type === "credentials") {
|
||||
// return token if credentials,saml-idp
|
||||
if (account.provider === "saml-idp") {
|
||||
return token;
|
||||
}
|
||||
// any other credentials, add user info
|
||||
return {
|
||||
...token,
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
impersonatedByUID: user?.impersonatedByUID,
|
||||
belongsToActiveTeam: user?.belongsToActiveTeam,
|
||||
};
|
||||
}
|
||||
|
||||
// The arguments above are from the provider so we need to look up the
|
||||
// user based on those values in order to construct a JWT.
|
||||
if (account.type === "oauth") {
|
||||
if (!account.provider || !account.providerAccountId) {
|
||||
return token;
|
||||
}
|
||||
const idP = account.provider === "saml" ? IdentityProvider.SAML : IdentityProvider.GOOGLE;
|
||||
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
identityProvider: idP,
|
||||
},
|
||||
{
|
||||
identityProviderId: account.providerAccountId as string,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingUser) {
|
||||
return await autoMergeIdentities();
|
||||
}
|
||||
|
||||
return {
|
||||
...token,
|
||||
id: existingUser.id,
|
||||
name: existingUser.name,
|
||||
username: existingUser.username,
|
||||
email: existingUser.email,
|
||||
role: existingUser.role,
|
||||
impersonatedByUID: token.impersonatedByUID as number,
|
||||
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
|
||||
};
|
||||
}
|
||||
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
const hasValidLicense = await checkLicense(prisma);
|
||||
const calendsoSession: Session = {
|
||||
...session,
|
||||
hasValidLicense,
|
||||
user: {
|
||||
...session.user,
|
||||
id: token.id as number,
|
||||
name: token.name,
|
||||
username: token.username as string,
|
||||
role: token.role as UserPermissionRole,
|
||||
impersonatedByUID: token.impersonatedByUID as number,
|
||||
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
|
||||
},
|
||||
};
|
||||
return calendsoSession;
|
||||
},
|
||||
async signIn(params) {
|
||||
const { user, account, profile } = params;
|
||||
|
||||
if (account?.provider === "email") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// In this case we've already verified the credentials in the authorize
|
||||
// callback so we can sign the user in.
|
||||
// Only if provider is not saml-idp
|
||||
if (account?.provider !== "saml-idp") {
|
||||
if (account?.type === "credentials") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (account?.type !== "oauth") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!user.email) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!user.name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (account?.provider) {
|
||||
let idP: IdentityProvider = IdentityProvider.GOOGLE;
|
||||
if (account.provider === "saml" || account.provider === "saml-idp") {
|
||||
idP = IdentityProvider.SAML;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error TODO validate email_verified key on profile
|
||||
user.email_verified = user.email_verified || !!user.emailVerified || profile.email_verified;
|
||||
|
||||
if (!user.email_verified) {
|
||||
return "/auth/error?error=unverified-email";
|
||||
}
|
||||
|
||||
// Only google oauth on this path
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
include: {
|
||||
accounts: {
|
||||
where: {
|
||||
provider: idP,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
identityProvider: idP as IdentityProvider,
|
||||
identityProviderId: account.providerAccountId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
// In this case there's an existing user and their email address
|
||||
// hasn't changed since they last logged in.
|
||||
if (existingUser.email === user.email) {
|
||||
try {
|
||||
// If old user without Account entry we link their google account
|
||||
if (existingUser.accounts.length === 0) {
|
||||
const linkAccountWithUserData = { ...account, userId: existingUser.id };
|
||||
await calcomAdapter.linkAccount(linkAccountWithUserData);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error("Error while linking account of already existing user");
|
||||
}
|
||||
}
|
||||
if (existingUser.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUser);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If the email address doesn't match, check if an account already exists
|
||||
// with the new email address. If it does, for now we return an error. If
|
||||
// not, update the email of their account and log them in.
|
||||
const userWithNewEmail = await prisma.user.findFirst({
|
||||
where: { email: user.email },
|
||||
});
|
||||
|
||||
if (!userWithNewEmail) {
|
||||
await prisma.user.update({ where: { id: existingUser.id }, data: { email: user.email } });
|
||||
if (existingUser.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUser);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return "/auth/error?error=new-email-conflict";
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no existing user for this identity provider and id, create
|
||||
// a new account. If an account already exists with the incoming email
|
||||
// address return an error for now.
|
||||
const existingUserWithEmail = await prisma.user.findFirst({
|
||||
where: { email: user.email },
|
||||
});
|
||||
|
||||
if (existingUserWithEmail) {
|
||||
// if self-hosted then we can allow auto-merge of identity providers if email is verified
|
||||
if (!hostedCal && existingUserWithEmail.emailVerified) {
|
||||
if (existingUserWithEmail.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUserWithEmail);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// check if user was invited
|
||||
if (
|
||||
!existingUserWithEmail.password &&
|
||||
!existingUserWithEmail.emailVerified &&
|
||||
!existingUserWithEmail.username
|
||||
) {
|
||||
await prisma.user.update({
|
||||
where: { email: user.email },
|
||||
data: {
|
||||
// Slugify the incoming name and append a few random characters to
|
||||
// prevent conflicts for users with the same name.
|
||||
username: usernameSlug(user.name),
|
||||
emailVerified: new Date(Date.now()),
|
||||
name: user.name,
|
||||
identityProvider: idP,
|
||||
identityProviderId: String(user.id),
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUserWithEmail.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUserWithEmail);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// User signs up with email/password and then tries to login with Google/SAML using the same email
|
||||
if (
|
||||
existingUserWithEmail.identityProvider === IdentityProvider.CAL &&
|
||||
(idP === IdentityProvider.GOOGLE || idP === IdentityProvider.SAML)
|
||||
) {
|
||||
await prisma.user.update({
|
||||
where: { email: existingUserWithEmail.email },
|
||||
data: { password: null },
|
||||
});
|
||||
if (existingUserWithEmail.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUserWithEmail);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) {
|
||||
return "/auth/error?error=use-password-login";
|
||||
}
|
||||
|
||||
return "/auth/error?error=use-identity-login";
|
||||
}
|
||||
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
// Slugify the incoming name and append a few random characters to
|
||||
// prevent conflicts for users with the same name.
|
||||
username: usernameSlug(user.name),
|
||||
emailVerified: new Date(Date.now()),
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
identityProvider: idP,
|
||||
identityProviderId: String(user.id),
|
||||
},
|
||||
});
|
||||
|
||||
const linkAccountNewUserData = { ...account, userId: newUser.id };
|
||||
await calcomAdapter.linkAccount(linkAccountNewUserData);
|
||||
|
||||
if (account.twoFactorEnabled) {
|
||||
return loginWithTotp(newUser);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
async redirect({ url, baseUrl }) {
|
||||
// Allows relative callback URLs
|
||||
if (url.startsWith("/")) return `${baseUrl}${url}`;
|
||||
// Allows callback URLs on the same domain
|
||||
else if (new URL(url).hostname === new URL(WEBAPP_URL).hostname) return url;
|
||||
return baseUrl;
|
||||
},
|
||||
},
|
||||
});
|
||||
export default NextAuth(AUTH_OPTIONS);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
import { ErrorCode, hashPassword, verifyPassword } from "../../../lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req: req });
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (!session || !session.user || !session.user.email) {
|
||||
res.status(401).json({ message: "Not authenticated" });
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { hashPassword } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(400).json({ message: "" });
|
||||
|
||||
@@ -2,8 +2,8 @@ import { IdentityProvider } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
import z from "zod";
|
||||
|
||||
import { isPasswordValid } from "@calcom/lib/auth";
|
||||
import { hashPassword } from "@calcom/lib/auth";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { hashPassword } from "@calcom/lib/auth";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { authenticator } from "otplib";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { ErrorCode, getSession, verifyPassword } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { authenticator } from "otplib";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { ErrorCode, getSession } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
@@ -3,17 +3,18 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { authenticator } from "otplib";
|
||||
import qrcode from "qrcode";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||
import { symmetricEncrypt } from "@calcom/lib/crypto";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { ErrorCode, getSession, verifyPassword } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
res.status(401).json({ message: "Not authenticated" });
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
async function handler(req: NextApiRequest & { userId?: number }) {
|
||||
const session = await getSession({ req });
|
||||
async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
|
||||
const session = await getServerSession({ req, res });
|
||||
/* To mimic API behavior and comply with types */
|
||||
req.userId = session?.user?.id || -1;
|
||||
const booking = await handleNewBooking(req, {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { defaultResponder, defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
async function handler(req: NextApiRequest & { userId?: number }) {
|
||||
const session = await getSession({ req });
|
||||
async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
|
||||
const session = await getServerSession({ req, res });
|
||||
/* To mimic API behavior and comply with types */
|
||||
req.userId = session?.user?.id || -1;
|
||||
return await handleCancelBooking(req);
|
||||
|
||||
@@ -60,7 +60,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
res.setHeader("Content-Type", "text/html");
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||
res.write(
|
||||
renderEmail("OrganizerScheduledEmail", {
|
||||
renderEmail("OrganizerRequestEmail", {
|
||||
calEvent: evt,
|
||||
attendee: evt.organizer,
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import type { Session } from "next-auth";
|
||||
|
||||
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
||||
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -42,7 +42,7 @@ const defaultIntegrationAddHandler = async ({
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
// Check that user is authenticated
|
||||
req.session = await getSession({ req });
|
||||
req.session = await getServerSession({ req, res });
|
||||
|
||||
const { args } = req.query;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
|
||||
let isCold = true;
|
||||
@@ -9,7 +9,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const prePrismaDate = performance.now();
|
||||
const prisma = (await import("@calcom/prisma")).default;
|
||||
const preSessionDate = performance.now();
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
if (!session) return res.status(409).json({ message: "Unauthorized" });
|
||||
const preUserDate = performance.now();
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
*/
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import * as trpcNext from "@calcom/trpc/server/adapters/next";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
import { createContext as createTrpcContext } from "@calcom/trpc/server/createContext";
|
||||
import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||
|
||||
export default trpcNext.createNextApiHandler({
|
||||
@@ -12,7 +13,11 @@ export default trpcNext.createNextApiHandler({
|
||||
/**
|
||||
* @link https://trpc.io/docs/context
|
||||
*/
|
||||
createContext,
|
||||
createContext: ({ req, res }) => {
|
||||
const sessionGetter = () => getServerSession({ req, res });
|
||||
|
||||
return createTrpcContext({ req, res }, sessionGetter);
|
||||
},
|
||||
/**
|
||||
* @link https://trpc.io/docs/error-handling
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,7 @@ import crypto from "crypto";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { defaultAvatarSrc } from "@lib/profile";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import RoutingFormsRoutingConfig from "@calcom/app-store/ee/routing-forms/pages/app-routing.config";
|
||||
import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config";
|
||||
import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
|
||||
|
||||
import type { AppProps } from "@lib/app-providers";
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
@@ -103,7 +103,7 @@ export async function getServerSideProps(
|
||||
appPages?: string[];
|
||||
}>
|
||||
) {
|
||||
const { params } = context;
|
||||
const { params, req, res } = context;
|
||||
if (!params) {
|
||||
return {
|
||||
notFound: true,
|
||||
@@ -120,7 +120,7 @@ export async function getServerSideProps(
|
||||
// appPages is actually hardcoded here and no matter the fileName the same variable would be used.
|
||||
// We can write some validation logic later on that ensures that [...appPages].tsx file exists
|
||||
params.appPages = pages.slice(1);
|
||||
const session = await getSession({ req: context.req });
|
||||
const session = await getServerSession({ req, res });
|
||||
const user = session?.user;
|
||||
|
||||
const result = await route.getServerSideProps(
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { GetStaticPaths, GetStaticPropsContext } from "next";
|
||||
import path from "path";
|
||||
|
||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||
import ExisitingGoogleCal from "@calcom/app-store/googlevideo/components/ExistingGoogleCal";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
@@ -36,11 +35,11 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||
isProOnly={data.isProOnly}
|
||||
images={source.data?.items as string[] | undefined}
|
||||
isTemplate={data.isTemplate}
|
||||
dependencies={data.dependencies}
|
||||
// tos="https://zoom.us/terms"
|
||||
// privacy="https://zoom.us/privacy"
|
||||
body={
|
||||
<>
|
||||
{data.slug === "google-meet" && <ExisitingGoogleCal />}
|
||||
<div dangerouslySetInnerHTML={{ __html: md.render(source.content) }} />
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { ChangeEventHandler } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { AppCategories } from "@calcom/prisma/client";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
@@ -86,9 +86,11 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, res } = context;
|
||||
|
||||
const ssg = await ssgInit(context);
|
||||
|
||||
const session = await getSession(context);
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
let appStore;
|
||||
if (session?.user?.id) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ResetPasswordRequest } from "@prisma/client";
|
||||
import debounce from "lodash/debounce";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getCsrfToken } from "next-auth/react";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import Link from "next/link";
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
@@ -21,7 +22,7 @@ type Props = {
|
||||
export default function Page({ resetPasswordRequest, csrfToken }: Props) {
|
||||
const { t } = useLocale();
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState<{ message: string } | null>(null);
|
||||
const [, setError] = React.useState<{ message: string } | null>(null);
|
||||
const [success, setSuccess] = React.useState(false);
|
||||
|
||||
const [password, setPassword] = React.useState("");
|
||||
@@ -185,6 +186,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
},
|
||||
id,
|
||||
csrfToken: await getCsrfToken({ req: context.req }),
|
||||
...(await serverSideTranslations(context.locale || "en", ["common"])),
|
||||
},
|
||||
};
|
||||
} catch (reason) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import debounce from "lodash/debounce";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getCsrfToken } from "next-auth/react";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import type { SyntheticEvent } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, EmailField } from "@calcom/ui";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
||||
@@ -132,17 +132,21 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
ForgotPassword.getInitialProps = async (context: GetServerSidePropsContext) => {
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, res } = context;
|
||||
const session = await getSession({ req });
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (session) {
|
||||
res.writeHead(302, { Location: "/" });
|
||||
res.end();
|
||||
return;
|
||||
return { props: {} };
|
||||
}
|
||||
|
||||
return {
|
||||
csrfToken: await getCsrfToken(context),
|
||||
props: {
|
||||
csrfToken: await getCsrfToken(context),
|
||||
...(await serverSideTranslations(context.locale || "en", ["common"])),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,8 +9,9 @@ import { FormProvider, useForm } from "react-hook-form";
|
||||
import { FaGoogle } from "react-icons/fa";
|
||||
|
||||
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { ErrorCode, getSession } from "@calcom/lib/auth";
|
||||
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -220,8 +221,9 @@ export default function Login({
|
||||
|
||||
// TODO: Once we understand how to retrieve prop types automatically from getServerSideProps, remove this temporary variable
|
||||
const _getServerSideProps = async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { req } = context;
|
||||
const session = await getSession({ req });
|
||||
const { req, res } = context;
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
const verifyJwt = (jwt: string) => {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
@@ -136,10 +136,12 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, res } = context;
|
||||
|
||||
const ssr = await ssrInit(context);
|
||||
const userCount = await prisma.user.count();
|
||||
const { req } = context;
|
||||
const session = await getSession({ req });
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (session?.user.role && session?.user.role !== UserPermissionRole.ADMIN) {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getProviders, signIn, getSession, getCsrfToken } from "next-auth/react";
|
||||
import { getProviders, signIn, getCsrfToken } from "next-auth/react";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
type Provider = {
|
||||
@@ -25,7 +26,9 @@ function signin({ providers }: { providers: Provider[] }) {
|
||||
export default signin;
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const session = await getSession(context);
|
||||
const { req, res } = context;
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
const csrfToken = await getCsrfToken(context);
|
||||
const providers = await getProviders();
|
||||
if (session) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||
import {
|
||||
hostedCal,
|
||||
@@ -16,7 +17,6 @@ import { checkUsername } from "@calcom/lib/server/checkUsername";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
@@ -60,9 +60,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
throw new Error(`File is not named sso/[provider]`);
|
||||
}
|
||||
|
||||
const { req } = context;
|
||||
const { req, res } = context;
|
||||
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
if (session) {
|
||||
|
||||
@@ -39,6 +39,7 @@ import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
@@ -46,7 +47,7 @@ import prisma from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { Badge, Button, EmailInput, HeadSeo } from "@calcom/ui";
|
||||
import { Button, EmailInput, HeadSeo, Badge } from "@calcom/ui";
|
||||
import { FiX, FiExternalLink, FiChevronLeft, FiCheck, FiCalendar } from "@calcom/ui/components/icon";
|
||||
|
||||
import { timeZone } from "@lib/clock";
|
||||
@@ -182,6 +183,7 @@ const querySchema = z.object({
|
||||
isSuccessBookingPage: z.string().optional(),
|
||||
formerTime: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
seatReferenceUid: z.string().optional(),
|
||||
});
|
||||
|
||||
export default function Success(props: SuccessProps) {
|
||||
@@ -194,11 +196,13 @@ export default function Success(props: SuccessProps) {
|
||||
changes,
|
||||
formerTime,
|
||||
email,
|
||||
seatReferenceUid,
|
||||
} = querySchema.parse(router.query);
|
||||
|
||||
if ((isCancellationMode || changes) && typeof window !== "undefined") {
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
const tz =
|
||||
(isSuccessBookingPage
|
||||
? props.bookingInfo.attendees.find((attendee) => attendee.email === email)?.timeZone
|
||||
: props.bookingInfo.eventType?.timeZone || props.bookingInfo.user?.timeZone) || timeZone();
|
||||
|
||||
const location = props.bookingInfo.location as ReturnType<typeof getEventLocationValue>;
|
||||
|
||||
@@ -206,10 +210,6 @@ export default function Success(props: SuccessProps) {
|
||||
props?.bookingInfo?.metadata || {}
|
||||
)?.videoCallUrl;
|
||||
|
||||
if (!location) {
|
||||
// Can't use logger.error because it throws error on client. stdout isn't available to it.
|
||||
console.error(`No location found `);
|
||||
}
|
||||
const status = props.bookingInfo?.status;
|
||||
const reschedule = props.bookingInfo.status === BookingStatus.ACCEPTED;
|
||||
const cancellationReason = props.bookingInfo.cancellationReason || props.bookingInfo.rejectionReason;
|
||||
@@ -232,12 +232,24 @@ export default function Success(props: SuccessProps) {
|
||||
const [calculatedDuration, setCalculatedDuration] = useState<number | undefined>(undefined);
|
||||
|
||||
function setIsCancellationMode(value: boolean) {
|
||||
if (value) router.query.cancel = "true";
|
||||
else delete router.query.cancel;
|
||||
router.replace({
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query },
|
||||
});
|
||||
const query_ = { ...router.query };
|
||||
|
||||
if (value) {
|
||||
query_.cancel = "true";
|
||||
} else {
|
||||
if (query_.cancel) {
|
||||
delete query_.cancel;
|
||||
}
|
||||
}
|
||||
|
||||
router.replace(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: { ...query_ },
|
||||
},
|
||||
undefined,
|
||||
{ scroll: false }
|
||||
);
|
||||
}
|
||||
|
||||
const eventNameObject = {
|
||||
@@ -246,6 +258,7 @@ export default function Success(props: SuccessProps) {
|
||||
eventName: (props.dynamicEventName as string) || props.eventType.eventName,
|
||||
host: props.profile.name || "Nameless",
|
||||
location: location,
|
||||
bookingFields: bookingInfo.responses,
|
||||
t,
|
||||
};
|
||||
|
||||
@@ -254,7 +267,16 @@ export default function Success(props: SuccessProps) {
|
||||
|
||||
const eventName = getEventName(eventNameObject, true);
|
||||
const needsConfirmation = eventType.requiresConfirmation && reschedule != true;
|
||||
const isCancelled = status === "CANCELLED" || status === "REJECTED";
|
||||
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
|
||||
|
||||
const isCancelled =
|
||||
status === "CANCELLED" ||
|
||||
status === "REJECTED" ||
|
||||
(isCancellationMode &&
|
||||
(!!seatReferenceUid
|
||||
? !bookingInfo.seatsReferences.some((reference) => reference.referenceUid === seatReferenceUid)
|
||||
: !userIsOwner));
|
||||
|
||||
const telemetry = useTelemetry();
|
||||
useEffect(() => {
|
||||
if (top !== window) {
|
||||
@@ -338,7 +360,6 @@ export default function Success(props: SuccessProps) {
|
||||
return t("emailed_you_and_attendees" + titleSuffix);
|
||||
}
|
||||
|
||||
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
|
||||
useTheme(isSuccessBookingPage ? props.profile.theme : "light");
|
||||
const title = t(
|
||||
`booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}`
|
||||
@@ -432,7 +453,9 @@ export default function Success(props: SuccessProps) {
|
||||
? t("submitted_recurring")
|
||||
: t("submitted")
|
||||
: isCancelled
|
||||
? t("event_cancelled")
|
||||
? seatReferenceUid
|
||||
? t("no_longer_attending")
|
||||
: t("event_cancelled")
|
||||
: props.recurringBookings
|
||||
? t("meeting_is_scheduled_recurring")
|
||||
: t("meeting_is_scheduled")}
|
||||
@@ -477,6 +500,7 @@ export default function Success(props: SuccessProps) {
|
||||
date={dayjs(formerTime)}
|
||||
is24h={is24h}
|
||||
isCancelled={isCancelled}
|
||||
tz={tz}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
@@ -488,31 +512,28 @@ export default function Success(props: SuccessProps) {
|
||||
date={date}
|
||||
is24h={is24h}
|
||||
isCancelled={isCancelled}
|
||||
tz={tz}
|
||||
/>
|
||||
</div>
|
||||
{(bookingInfo?.user || bookingInfo?.attendees) && (
|
||||
<>
|
||||
<div className="font-medium">{t("who")}</div>
|
||||
<div className="col-span-2 last:mb-0">
|
||||
<>
|
||||
{bookingInfo?.user && (
|
||||
<div className="mb-3">
|
||||
<p>
|
||||
<span className="mr-2">{bookingInfo.user.name}</span>
|
||||
<Badge variant="blue" bold>
|
||||
{t("Host")}
|
||||
</Badge>
|
||||
</p>
|
||||
<p className="text-bookinglight">{bookingInfo.user.email}</p>
|
||||
{bookingInfo?.user && (
|
||||
<div className="mb-3">
|
||||
<div>
|
||||
<span className="mr-2">{bookingInfo.user.name}</span>
|
||||
<Badge variant="blue">{t("Host")}</Badge>
|
||||
</div>
|
||||
)}
|
||||
{bookingInfo?.attendees.map((attendee) => (
|
||||
<div key={attendee.name} className="mb-3 last:mb-0">
|
||||
{attendee.name && <p>{attendee.name}</p>}
|
||||
<p className="text-bookinglight">{attendee.email}</p>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
<p className="text-bookinglight">{bookingInfo.user.email}</p>
|
||||
</div>
|
||||
)}
|
||||
{bookingInfo?.attendees.map((attendee) => (
|
||||
<div key={attendee.name} className="mb-3 last:mb-0">
|
||||
{attendee.name && <p>{attendee.name}</p>}
|
||||
<p className="text-bookinglight">{attendee.email}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -586,7 +607,9 @@ export default function Success(props: SuccessProps) {
|
||||
{!props.recurringBookings && (
|
||||
<span className="text-bookinglight inline text-gray-700">
|
||||
<span className="underline">
|
||||
<Link href={`/reschedule/${bookingInfo?.uid}`} legacyBehavior>
|
||||
<Link
|
||||
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}`}
|
||||
legacyBehavior>
|
||||
{t("reschedule")}
|
||||
</Link>
|
||||
</span>
|
||||
@@ -616,6 +639,7 @@ export default function Success(props: SuccessProps) {
|
||||
setIsCancellationMode={setIsCancellationMode}
|
||||
theme={isSuccessBookingPage ? props.profile.theme : "light"}
|
||||
allRemainingBookings={allRemainingBookings}
|
||||
seatReferenceUid={seatReferenceUid}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
@@ -623,7 +647,7 @@ export default function Success(props: SuccessProps) {
|
||||
!needsConfirmation &&
|
||||
!isCancellationMode &&
|
||||
!isCancelled &&
|
||||
calculatedDuration && (
|
||||
!!calculatedDuration && (
|
||||
<>
|
||||
<hr className="border-bookinglightest dark:border-darkgray-300 mt-8" />
|
||||
<div className="text-bookingdark align-center flex flex-row justify-center pt-8">
|
||||
@@ -731,6 +755,7 @@ export default function Success(props: SuccessProps) {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{session === null && !(userIsOwner || props.hideBranding) && (
|
||||
<>
|
||||
<hr className="border-bookinglightest dark:border-darkgray-300 mt-8" />
|
||||
@@ -785,6 +810,7 @@ type RecurringBookingsProps = {
|
||||
is24h: boolean;
|
||||
allRemainingBookings: boolean;
|
||||
isCancelled: boolean;
|
||||
tz: string;
|
||||
};
|
||||
|
||||
export function RecurringBookings({
|
||||
@@ -795,6 +821,7 @@ export function RecurringBookings({
|
||||
allRemainingBookings,
|
||||
is24h,
|
||||
isCancelled,
|
||||
tz,
|
||||
}: RecurringBookingsProps) {
|
||||
const [moreEventsVisible, setMoreEventsVisible] = useState(false);
|
||||
const {
|
||||
@@ -822,12 +849,12 @@ export function RecurringBookings({
|
||||
{eventType.recurringEvent?.count &&
|
||||
recurringBookingsSorted.slice(0, 4).map((dateStr: string, idx: number) => (
|
||||
<div key={idx} className={classNames("mb-2", isCancelled ? "line-through" : "")}>
|
||||
{formatToLocalizedDate(dayjs.tz(dateStr, timeZone()), language, "full")}
|
||||
{formatToLocalizedDate(dayjs.tz(dateStr, tz), language, "full", tz)}
|
||||
<br />
|
||||
{formatToLocalizedTime(dayjs(dateStr), language, undefined, !is24h)} -{" "}
|
||||
{formatToLocalizedTime(dayjs(dateStr).add(duration, "m"), language, undefined, !is24h)}{" "}
|
||||
{formatToLocalizedTime(dayjs(dateStr), language, undefined, !is24h, tz)} -{" "}
|
||||
{formatToLocalizedTime(dayjs(dateStr).add(duration, "m"), language, undefined, !is24h, tz)}{" "}
|
||||
<span className="text-bookinglight">
|
||||
({formatToLocalizedTimezone(dayjs(dateStr), language, timeZone())})
|
||||
({formatToLocalizedTimezone(dayjs(dateStr), language, tz)})
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -842,12 +869,12 @@ export function RecurringBookings({
|
||||
{eventType.recurringEvent?.count &&
|
||||
recurringBookingsSorted.slice(4).map((dateStr: string, idx: number) => (
|
||||
<div key={idx} className={classNames("mb-2", isCancelled ? "line-through" : "")}>
|
||||
{formatToLocalizedDate(dayjs.tz(date, timeZone()), language, "full")}
|
||||
{formatToLocalizedDate(dayjs.tz(date, tz), language, "full", tz)}
|
||||
<br />
|
||||
{formatToLocalizedTime(date, language, undefined, !is24h)} -{" "}
|
||||
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h)}{" "}
|
||||
{formatToLocalizedTime(date, language, undefined, !is24h, tz)} -{" "}
|
||||
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h, tz)}{" "}
|
||||
<span className="text-bookinglight">
|
||||
({formatToLocalizedTimezone(dayjs(dateStr), language, timeZone())})
|
||||
({formatToLocalizedTimezone(dayjs(dateStr), language, tz)})
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -860,11 +887,11 @@ export function RecurringBookings({
|
||||
|
||||
return (
|
||||
<div className={classNames(isCancelled ? "line-through" : "")}>
|
||||
{formatToLocalizedDate(dayjs.tz(date, timeZone()), language, "full")}
|
||||
{formatToLocalizedDate(date, language, "full", tz)}
|
||||
<br />
|
||||
{formatToLocalizedTime(date, language, undefined, !is24h)} -{" "}
|
||||
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h)}{" "}
|
||||
<span className="text-bookinglight">({formatToLocalizedTimezone(date, language, timeZone())})</span>
|
||||
{formatToLocalizedTime(date, language, undefined, !is24h, tz)} -{" "}
|
||||
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h, tz)}{" "}
|
||||
<span className="text-bookinglight">({formatToLocalizedTimezone(date, language, tz)})</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -901,6 +928,7 @@ const getEventTypesFromDB = async (id: number) => {
|
||||
currency: true,
|
||||
bookingFields: true,
|
||||
disableGuests: true,
|
||||
timeZone: true,
|
||||
owner: {
|
||||
select: userSelect,
|
||||
},
|
||||
@@ -995,7 +1023,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
|
||||
const bookingInfoRaw = await prisma.booking.findFirst({
|
||||
where: {
|
||||
uid,
|
||||
uid: await maybeGetBookingUidFromSeat(prisma, uid),
|
||||
},
|
||||
select: {
|
||||
title: true,
|
||||
@@ -1019,12 +1047,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
name: true,
|
||||
email: true,
|
||||
username: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
eventTypeId: true,
|
||||
@@ -1032,6 +1062,12 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
select: {
|
||||
eventName: true,
|
||||
slug: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
seatsReferences: {
|
||||
select: {
|
||||
referenceUid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1103,19 +1139,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
handleSeatsEventTypeOnBooking(eventType, bookingInfo, email);
|
||||
}
|
||||
|
||||
let recurringBookings = null;
|
||||
if (bookingInfo.recurringEventId) {
|
||||
// We need to get the dates for the bookings to be able to show them in the UI
|
||||
recurringBookings = await prisma.booking.findMany({
|
||||
where: {
|
||||
recurringEventId: bookingInfo.recurringEventId,
|
||||
},
|
||||
select: {
|
||||
startTime: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const payment = await prisma.payment.findFirst({
|
||||
where: {
|
||||
bookingId: bookingInfo.id,
|
||||
@@ -1125,21 +1148,30 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
refunded: true,
|
||||
},
|
||||
});
|
||||
const paymentStatus = {
|
||||
success: payment?.success,
|
||||
refunded: payment?.refunded,
|
||||
};
|
||||
|
||||
return {
|
||||
props: {
|
||||
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
|
||||
profile,
|
||||
eventType,
|
||||
recurringBookings: recurringBookings ? recurringBookings.map((obj) => obj.startTime.toString()) : null,
|
||||
recurringBookings: await getRecurringBookings(bookingInfo.recurringEventId),
|
||||
trpcState: ssr.dehydrate(),
|
||||
dynamicEventName: bookingInfo?.eventType?.eventName || "",
|
||||
bookingInfo,
|
||||
paymentStatus: payment ? paymentStatus : null,
|
||||
paymentStatus: payment,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getRecurringBookings(recurringEventId: string | null) {
|
||||
if (!recurringEventId) return null;
|
||||
const recurringBookings = await prisma.booking.findMany({
|
||||
where: {
|
||||
recurringEventId,
|
||||
},
|
||||
select: {
|
||||
startTime: true,
|
||||
},
|
||||
});
|
||||
return recurringBookings.map((obj) => obj.startTime.toString());
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { PeriodType } from "@prisma/client";
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import type { SchedulingType } from "@prisma/client";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -10,22 +10,23 @@ import { z } from "zod";
|
||||
|
||||
import { validateCustomEventName } from "@calcom/core/event";
|
||||
import type { EventLocationType } from "@calcom/core/location";
|
||||
import { validateBookingLimitOrder } from "@calcom/lib";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import getEventTypeById from "@calcom/lib/getEventTypeById";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { BookingLimit, RecurringEvent } from "@calcom/types/Calendar";
|
||||
import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Form, showToast } from "@calcom/ui";
|
||||
|
||||
import { asStringOrThrow } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import { AvailabilityTab } from "@components/eventtype/AvailabilityTab";
|
||||
@@ -85,9 +86,9 @@ export type FormValues = {
|
||||
externalId: string;
|
||||
};
|
||||
successRedirectUrl: string;
|
||||
bookingLimits?: BookingLimit;
|
||||
hosts: { userId: number }[];
|
||||
hostsFixed: { userId: number }[];
|
||||
durationLimits?: IntervalLimit;
|
||||
bookingLimits?: IntervalLimit;
|
||||
hosts: { userId: number; isFixed: boolean }[];
|
||||
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
||||
};
|
||||
|
||||
@@ -181,6 +182,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
delete metadata.config?.useHostSchedulesForTeamEvent;
|
||||
}
|
||||
|
||||
const bookingFields: Prisma.JsonObject = {};
|
||||
|
||||
eventType.bookingFields.forEach(({ name }) => {
|
||||
bookingFields[name] = name;
|
||||
});
|
||||
|
||||
const defaultValues = {
|
||||
title: eventType.title,
|
||||
locations: eventType.locations || [],
|
||||
@@ -188,6 +195,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
description: eventType.description ?? undefined,
|
||||
schedule: eventType.schedule || undefined,
|
||||
bookingLimits: eventType.bookingLimits || undefined,
|
||||
durationLimits: eventType.durationLimits || undefined,
|
||||
length: eventType.length,
|
||||
hidden: eventType.hidden,
|
||||
periodDates: {
|
||||
@@ -200,16 +208,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
schedulingType: eventType.schedulingType,
|
||||
minimumBookingNotice: eventType.minimumBookingNotice,
|
||||
metadata,
|
||||
hosts: !!eventType.hosts?.length
|
||||
? eventType.hosts.filter((host) => !host.isFixed)
|
||||
: eventType.users
|
||||
.filter(() => eventType.schedulingType === SchedulingType.ROUND_ROBIN)
|
||||
.map((user) => ({ userId: user.id })),
|
||||
hostsFixed: !!eventType.hosts?.length
|
||||
? eventType.hosts.filter((host) => host.isFixed)
|
||||
: eventType.users
|
||||
.filter(() => eventType.schedulingType === SchedulingType.COLLECTIVE)
|
||||
.map((user) => ({ userId: user.id })),
|
||||
hosts: eventType.hosts,
|
||||
} as const;
|
||||
|
||||
const formMethods = useForm<FormValues>({
|
||||
@@ -221,9 +220,13 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
// Make it optional because it's not submitted from all tabs of the page
|
||||
eventName: z
|
||||
.string()
|
||||
.refine((val) => validateCustomEventName(val, t("invalid_event_name_variables")) === true, {
|
||||
message: t("invalid_event_name_variables"),
|
||||
})
|
||||
.refine(
|
||||
(val) =>
|
||||
validateCustomEventName(val, t("invalid_event_name_variables"), bookingFields) === true,
|
||||
{
|
||||
message: t("invalid_event_name_variables"),
|
||||
}
|
||||
)
|
||||
.optional(),
|
||||
length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
|
||||
bookingFields: eventTypeBookingFields,
|
||||
@@ -302,12 +305,11 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
recurringEvent,
|
||||
locations,
|
||||
metadata,
|
||||
customInputs,
|
||||
hosts: hostsInput,
|
||||
hostsFixed,
|
||||
// We don't need to send send these values to the backend
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
seatsPerTimeSlotEnabled,
|
||||
@@ -316,14 +318,14 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
...input
|
||||
} = values;
|
||||
|
||||
const hosts: ((typeof hostsInput)[number] & { isFixed?: boolean })[] = [];
|
||||
if (hostsInput || hostsFixed) {
|
||||
hosts.push(...hostsInput.concat(hostsFixed.map((host) => ({ isFixed: true, ...host }))));
|
||||
if (bookingLimits) {
|
||||
const isValid = validateIntervalLimitOrder(bookingLimits);
|
||||
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
|
||||
}
|
||||
|
||||
if (bookingLimits) {
|
||||
const isValid = validateBookingLimitOrder(bookingLimits);
|
||||
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
|
||||
if (durationLimits) {
|
||||
const isValid = validateIntervalLimitOrder(durationLimits);
|
||||
if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
|
||||
}
|
||||
|
||||
if (metadata?.multipleDuration !== undefined) {
|
||||
@@ -338,7 +340,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
|
||||
updateMutation.mutate({
|
||||
...input,
|
||||
hosts,
|
||||
locations,
|
||||
recurringEvent,
|
||||
periodStartDate: periodDates.startDate,
|
||||
@@ -348,6 +349,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
beforeEventBuffer: beforeBufferTime,
|
||||
afterEventBuffer: afterBufferTime,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
metadata,
|
||||
@@ -369,8 +371,10 @@ const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) =
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, query } = context;
|
||||
const session = await getSession({ req });
|
||||
const { req, res, query } = context;
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
const typeParam = parseInt(asStringOrThrow(query.type));
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
@@ -391,9 +395,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
try {
|
||||
const res = await getEventTypeById({ eventTypeId: typeParam, userId: session.user.id, prisma });
|
||||
|
||||
return {
|
||||
props: {
|
||||
session,
|
||||
// session,
|
||||
type: typeParam,
|
||||
trpcState: ssr.dehydrate(),
|
||||
initialData: {
|
||||
|
||||
@@ -338,7 +338,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
<MemoizedItem type={type} group={group} readOnly={readOnly} />
|
||||
<div className="mt-4 hidden sm:mt-0 sm:flex">
|
||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
{type.users?.length > 1 && (
|
||||
{type.team && (
|
||||
<AvatarGroup
|
||||
className="relative top-1 right-3"
|
||||
size="sm"
|
||||
|
||||
@@ -4,7 +4,7 @@ import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -140,8 +140,10 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, res } = context;
|
||||
|
||||
const crypto = await import("crypto");
|
||||
const session = await getSession(context);
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { NextPageContext } from "next";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
|
||||
function RedirectPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: NextPageContext) {
|
||||
const session = await getSession(context);
|
||||
export async function getServerSideProps({ req, res }: GetServerSidePropsContext) {
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
}
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { URLSearchParams } from "url";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
|
||||
import { asStringOrUndefined } from "@lib/asStringOrNull";
|
||||
|
||||
export default function Type() {
|
||||
// Just redirect to the schedule page to reschedule it.
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { uid: bookingId } = z
|
||||
.object({ uid: z.string(), seatReferenceUid: z.string().optional() })
|
||||
.parse(context.query);
|
||||
let seatReferenceUid;
|
||||
const uid = await maybeGetBookingUidFromSeat(prisma, bookingId);
|
||||
if (uid) {
|
||||
seatReferenceUid = bookingId;
|
||||
}
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
uid: asStringOrUndefined(context.query.uid),
|
||||
uid,
|
||||
},
|
||||
select: {
|
||||
...bookingMinimalSelect,
|
||||
@@ -42,9 +51,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
if (!booking) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as {
|
||||
notFound: true;
|
||||
};
|
||||
} as const;
|
||||
}
|
||||
|
||||
if (!booking?.eventType && !booking?.dynamicEventSlugRef) {
|
||||
@@ -66,9 +73,16 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
: booking.user?.username || "rick") /* This shouldn't happen */ +
|
||||
"/" +
|
||||
eventType?.slug;
|
||||
const destinationUrl = new URLSearchParams();
|
||||
if (seatReferenceUid) {
|
||||
destinationUrl.set("rescheduleUid", seatReferenceUid);
|
||||
} else {
|
||||
destinationUrl.set("rescheduleUid", bookingId);
|
||||
}
|
||||
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/" + eventPage + "?rescheduleUid=" + context.query.uid,
|
||||
destination: `/${eventPage}?${destinationUrl.toString()}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -69,7 +69,10 @@ const ConferencingLayout = () => {
|
||||
}, []);
|
||||
|
||||
const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({
|
||||
onSuccess: onSuccessCallback,
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
onSuccessCallback();
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(`Error: ${error.message}`, "error");
|
||||
},
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { BaseSyntheticEvent } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { ErrorCode } from "@calcom/lib/auth";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
|
||||
@@ -2,8 +2,8 @@ import { IdentityProvider } from "@prisma/client";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { identityProviderNameMap } from "@calcom/features/auth/lib/identityProviderNameMap";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { identityProviderNameMap } from "@calcom/lib/auth";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
@@ -7,13 +7,14 @@ import { useEffect } from "react";
|
||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { Avatar, Button, HeadSeo, AvatarGroup } from "@calcom/ui";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { Avatar, AvatarGroup, Button, EmptyScreen, HeadSeo } from "@calcom/ui";
|
||||
import { FiArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
||||
@@ -24,13 +25,15 @@ import Team from "@components/team/screens/Team";
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export type TeamPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
function TeamPage({ team }: TeamPageProps) {
|
||||
function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
||||
useTheme(team.theme);
|
||||
const showMembers = useToggleQuery("members");
|
||||
const { t } = useLocale();
|
||||
const isEmbed = useIsEmbed();
|
||||
const telemetry = useTelemetry();
|
||||
const router = useRouter();
|
||||
const teamName = team.name || "Nameless Team";
|
||||
const isBioEmpty = !team.bio || !team.bio.replace("<p><br></p>", "").length;
|
||||
|
||||
useEffect(() => {
|
||||
telemetry.event(
|
||||
@@ -39,6 +42,18 @@ function TeamPage({ team }: TeamPageProps) {
|
||||
);
|
||||
}, [telemetry, router.asPath]);
|
||||
|
||||
if (isUnpublished) {
|
||||
return (
|
||||
<div className="m-8 flex items-center justify-center">
|
||||
<EmptyScreen
|
||||
avatar={<Avatar alt={teamName} imageSrc={getPlaceholderAvatar(team.logo, team.name)} size="lg" />}
|
||||
headline={t("team_is_unpublished", { team: teamName })}
|
||||
description={t("team_is_unpublished_description")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const EventTypes = () => (
|
||||
<ul className="dark:border-darkgray-300 rounded-md border border-gray-200">
|
||||
{team.eventTypes.map((type, index) => (
|
||||
@@ -48,40 +63,38 @@ function TeamPage({ team }: TeamPageProps) {
|
||||
"dark:bg-darkgray-100 dark:border-darkgray-300 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50",
|
||||
!isEmbed && "bg-white"
|
||||
)}>
|
||||
<Link
|
||||
href={`/team/${team.slug}/${type.slug}`}
|
||||
className="flex justify-between px-6 py-4"
|
||||
data-testid="event-type-link">
|
||||
<div className="flex-shrink">
|
||||
<div className="flex flex-wrap items-center space-x-2 rtl:space-x-reverse">
|
||||
<h2 className="dark:text-darkgray-700 text-sm font-semibold text-gray-700">{type.title}</h2>
|
||||
<div className="px-6 py-4 ">
|
||||
<Link
|
||||
href={`/team/${team.slug}/${type.slug}`}
|
||||
data-testid="event-type-link"
|
||||
className="flex justify-between">
|
||||
<div className="flex-shrink">
|
||||
<div className="flex flex-wrap items-center space-x-2 rtl:space-x-reverse">
|
||||
<h2 className="dark:text-darkgray-700 text-sm font-semibold text-gray-700">{type.title}</h2>
|
||||
</div>
|
||||
<EventTypeDescription className="text-sm" eventType={type} />
|
||||
</div>
|
||||
<EventTypeDescription className="text-sm" eventType={type} />
|
||||
</div>
|
||||
<div className="mt-1 self-center">
|
||||
<AvatarGroup
|
||||
truncateAfter={4}
|
||||
className="flex flex-shrink-0"
|
||||
size="sm"
|
||||
items={type.users.map((user) => ({
|
||||
alt: user.name || "",
|
||||
title: user.name || "",
|
||||
image: CAL_URL + "/" + user.username + "/avatar.png" || "",
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="mt-1 self-center">
|
||||
<AvatarGroup
|
||||
truncateAfter={4}
|
||||
className="flex flex-shrink-0"
|
||||
size="sm"
|
||||
items={type.users.map((user) => ({
|
||||
alt: user.name || "",
|
||||
title: user.name || "",
|
||||
image: CAL_URL + "/" + user.username + "/avatar.png" || "",
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
const teamName = team.name || "Nameless Team";
|
||||
|
||||
const isBioEmpty = !team.bio || !team.bio.replace("<p><br></p>", "").length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<HeadSeo
|
||||
title={teamName}
|
||||
description={teamName}
|
||||
@@ -122,7 +135,6 @@ function TeamPage({ team }: TeamPageProps) {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="mt-8 flex justify-center text-center dark:text-white">
|
||||
<Button
|
||||
color="minimal"
|
||||
@@ -138,7 +150,7 @@ function TeamPage({ team }: TeamPageProps) {
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,6 +158,25 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
const ssr = await ssrInit(context);
|
||||
const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug;
|
||||
|
||||
const unpublishedTeam = await prisma.team.findFirst({
|
||||
where: {
|
||||
metadata: {
|
||||
path: ["requestedSlug"],
|
||||
equals: slug,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (unpublishedTeam) {
|
||||
return {
|
||||
props: {
|
||||
isUnpublished: true,
|
||||
team: unpublishedTeam,
|
||||
trpcState: ssr.dehydrate(),
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
const team = await getTeamWithMembers(undefined, slug);
|
||||
|
||||
if (!team) return { notFound: true } as { notFound: true };
|
||||
@@ -163,7 +194,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
team,
|
||||
trpcState: ssr.dehydrate(),
|
||||
},
|
||||
};
|
||||
} as const;
|
||||
};
|
||||
|
||||
export default TeamPage;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import { privacyFilteredLocations } from "@calcom/app-store/locations";
|
||||
@@ -24,6 +25,11 @@ export default function TeamBookingPage(props: TeamBookingPageProps) {
|
||||
|
||||
TeamBookingPage.isThemeSupported = true;
|
||||
|
||||
const querySchema = z.object({
|
||||
rescheduleUid: z.string().optional(),
|
||||
bookingUid: z.string().optional(),
|
||||
});
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const ssr = await ssrInit(context);
|
||||
const eventTypeId = parseInt(asStringOrThrow(context.query.type));
|
||||
@@ -119,8 +125,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
})[0];
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (context.query.rescheduleUid) {
|
||||
booking = await getBooking(prisma, context.query.rescheduleUid as string, eventTypeObject.bookingFields);
|
||||
const { rescheduleUid, bookingUid } = querySchema.parse(context.query);
|
||||
if (rescheduleUid || bookingUid) {
|
||||
booking = await getBooking(prisma, rescheduleUid || bookingUid || "", eventTypeObject.bookingFields);
|
||||
}
|
||||
|
||||
// Checking if number of recurring event ocurrances is valid against event type configuration
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import DailyIframe from "@daily-co/daily-js";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import { useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/date-fns";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { FiChevronRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export type JoinCallPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
export default function JoinCall(props: JoinCallPageProps) {
|
||||
const { t } = useLocale();
|
||||
const { meetingUrl, meetingPassword } = props;
|
||||
const { meetingUrl, meetingPassword, booking } = props;
|
||||
|
||||
useEffect(() => {
|
||||
const callFrame = DailyIframe.createFrame({
|
||||
@@ -53,7 +59,6 @@ export default function JoinCall(props: JoinCallPageProps) {
|
||||
<>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={APP_NAME + " Video"} />
|
||||
<meta name="description" content={t("quick_video_meeting")} />
|
||||
<meta property="og:image" content={SEO_IMG_OGIMG_VIDEO} />
|
||||
<meta property="og:type" content="website" />
|
||||
@@ -77,11 +82,119 @@ export default function JoinCall(props: JoinCallPageProps) {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<VideoMeetingInfo booking={booking} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProgressBarProps {
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}
|
||||
|
||||
function ProgressBar(props: ProgressBarProps) {
|
||||
const { startTime, endTime } = props;
|
||||
const currentTime = dayjs().second(0).millisecond(0);
|
||||
const startingTime = dayjs(startTime).second(0).millisecond(0);
|
||||
const isPast = currentTime.isAfter(startingTime);
|
||||
const currentDifference = dayjs().diff(startingTime, "minutes");
|
||||
const startDuration = dayjs(endTime).diff(startingTime, "minutes");
|
||||
const [duration] = useState(() => {
|
||||
if (currentDifference >= 0 && isPast) {
|
||||
return startDuration - currentDifference;
|
||||
} else {
|
||||
return startDuration;
|
||||
}
|
||||
});
|
||||
|
||||
const prev = startDuration - duration;
|
||||
const percentage = prev * (100 / startDuration);
|
||||
return (
|
||||
<div>
|
||||
<p>{duration} minutes</p>
|
||||
<div className="relative h-2 max-w-xl overflow-hidden rounded-full">
|
||||
<div className="absolute h-full w-full bg-gray-500/10" />
|
||||
<div className={classNames("relative h-full bg-green-500")} style={{ width: `${percentage}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface VideoMeetingInfo {
|
||||
booking: JoinCallPageProps["booking"];
|
||||
}
|
||||
|
||||
export function VideoMeetingInfo(props: VideoMeetingInfo) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { booking } = props;
|
||||
|
||||
const endTime = new Date(booking.endTime);
|
||||
const startTime = new Date(booking.startTime);
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside
|
||||
className={classNames(
|
||||
"no-scrollbar fixed top-0 left-0 z-30 flex h-full w-64 transform justify-between overflow-x-hidden overflow-y-scroll transition-all duration-300 ease-in-out",
|
||||
open ? "translate-x-0" : "-translate-x-[232px]"
|
||||
)}>
|
||||
<main className="prose-sm prose max-w-64 prose-a:text-white prose-h3:text-white prose-h3:font-cal scroll-bar scrollbar-track-w-20 w-full overflow-scroll overflow-x-hidden border-r border-gray-300/20 bg-black/80 p-4 text-white shadow-sm backdrop-blur-lg">
|
||||
<h3>What:</h3>
|
||||
<p>{booking.title}</p>
|
||||
<h3>Invitee Time Zone:</h3>
|
||||
<p>{booking.user?.timeZone}</p>
|
||||
<h3>When:</h3>
|
||||
<p>
|
||||
{formatToLocalizedDate(startTime)} <br />
|
||||
{formatToLocalizedTime(startTime)}
|
||||
</p>
|
||||
<h3>Time left</h3>
|
||||
<ProgressBar
|
||||
key={String(open)}
|
||||
endTime={endTime.toISOString()}
|
||||
startTime={startTime.toISOString()}
|
||||
/>
|
||||
|
||||
<h3>Who:</h3>
|
||||
<p>
|
||||
{booking?.user?.name} - Organizer{" "}
|
||||
<a href={`mailto:${booking?.user?.email}`}>{booking?.user?.email}</a>
|
||||
</p>
|
||||
|
||||
{booking.attendees.length
|
||||
? booking.attendees.map((attendee) => (
|
||||
<p key={attendee.id}>
|
||||
{attendee.name} – <a href={`mailto:${attendee.email}`}>{attendee.email}</a>
|
||||
</p>
|
||||
))
|
||||
: null}
|
||||
|
||||
<h3>Description</h3>
|
||||
|
||||
<div
|
||||
className="prose-sm prose prose-invert"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(booking.description ?? "") }}
|
||||
/>
|
||||
</main>
|
||||
<div className="flex items-center justify-center">
|
||||
<button
|
||||
aria-label={`${open ? "close" : "open"} booking description sidebar`}
|
||||
className="h-20 w-6 rounded-r-md border border-l-0 border-gray-300/20 bg-black/60 text-white shadow-sm backdrop-blur-lg"
|
||||
onClick={() => setOpen(!open)}>
|
||||
<FiChevronRight
|
||||
aria-hidden
|
||||
className={classNames(open && "rotate-180", "w-5 transition-all duration-300 ease-in-out")}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { req, res } = context;
|
||||
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
@@ -91,10 +204,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
select: {
|
||||
...bookingMinimalSelect,
|
||||
uid: true,
|
||||
description: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
credentials: true,
|
||||
timeZone: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
references: {
|
||||
@@ -139,7 +256,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
startTime: booking.startTime.toString(),
|
||||
endTime: booking.endTime.toString(),
|
||||
});
|
||||
const session = await getSession();
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
// set meetingPassword to null for guests
|
||||
if (session?.user.id !== bookingObj.user?.id) {
|
||||
@@ -154,6 +272,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
...(typeof bookingObj.references[0].meetingPassword === "string" && {
|
||||
meetingPassword: bookingObj.references[0].meetingPassword,
|
||||
}),
|
||||
booking: bookingObj,
|
||||
trpcState: ssr.dehydrate(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,63 +5,35 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { Button, HeadSeo } from "@calcom/ui";
|
||||
import { FiArrowRight, FiCalendar, FiX } from "@calcom/ui/components/icon";
|
||||
import { Button, HeadSeo, EmptyScreen } from "@calcom/ui";
|
||||
import { FiArrowRight, FiCalendar, FiClock } from "@calcom/ui/components/icon";
|
||||
|
||||
export default function MeetingNotStarted(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div>
|
||||
<HeadSeo title="Meeting Unavailable" description="Meeting Unavailable" />
|
||||
<>
|
||||
<HeadSeo title={t("this_meeting_has_not_started_yet")} description={props.booking.title} />
|
||||
<main className="mx-auto my-24 max-w-3xl">
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div className="fixed inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
|
||||
<span className="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
<div
|
||||
className="inline-block transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6 sm:align-middle"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-headline">
|
||||
<div>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
||||
<FiX className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-headline">
|
||||
This meeting has not started yet
|
||||
</h3>
|
||||
</div>
|
||||
<div className="mt-4 border-t border-b py-4">
|
||||
<h2 className="font-cal mb-2 text-center text-lg font-medium text-gray-600">
|
||||
{props.booking.title}
|
||||
</h2>
|
||||
<p className="text-center text-gray-500">
|
||||
<FiCalendar className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
{dayjs(props.booking.startTime).format(detectBrowserTimeFormat + ", dddd DD MMMM YYYY")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<p className="text-sm text-gray-500">
|
||||
This meeting will be accessible 60 minutes in advance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 text-center sm:mt-6">
|
||||
<div className="mt-5">
|
||||
<Button data-testid="return-home" href="/event-types" EndIcon={FiArrowRight}>
|
||||
{t("go_back")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EmptyScreen
|
||||
Icon={FiClock}
|
||||
headline={t("this_meeting_has_not_started_yet")}
|
||||
description={
|
||||
<>
|
||||
<h2 className="mb-2 text-center font-medium">{props.booking.title}</h2>
|
||||
<p className="text-center text-gray-500">
|
||||
<FiCalendar className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
{dayjs(props.booking.startTime).format(detectBrowserTimeFormat + ", dddd DD MMMM YYYY")}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
buttonRaw={
|
||||
<Button data-testid="return-home" href="/event-types" EndIcon={FiArrowRight}>
|
||||
{t("go_back")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +1,25 @@
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, HeadSeo } from "@calcom/ui";
|
||||
import { Button, EmptyScreen, HeadSeo } from "@calcom/ui";
|
||||
import { FiX, FiArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
export default function NoMeetingFound() {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<HeadSeo title={t("no_meeting_found")} description={t("no_meeting_found")} />
|
||||
<main className="mx-auto my-24 max-w-3xl">
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div className="fixed inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
|
||||
<span className="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
<div
|
||||
className="inline-block transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6 sm:align-middle"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-headline">
|
||||
<div>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
||||
<FiX className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-headline">
|
||||
{t("no_meeting_found")}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<p className="text-center text-sm text-gray-500">{t("no_meeting_found_description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 text-center sm:mt-6">
|
||||
<div className="mt-5">
|
||||
<Button data-testid="return-home" href="/event-types" EndIcon={FiArrowRight}>
|
||||
{t("go_back_home")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EmptyScreen
|
||||
Icon={FiX}
|
||||
headline={t("no_meeting_found")}
|
||||
description={t("no_meeting_found_description")}
|
||||
buttonRaw={
|
||||
<Button data-testid="return-home" href="/event-types" EndIcon={FiArrowRight}>
|
||||
{t("go_back_home")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user