Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a810a538e9 | ||
|
|
1cd0d7b5ca | ||
|
|
4540c79580 | ||
|
|
00d01882d4 | ||
|
|
6ce4122528 | ||
|
|
7ddbcfcdcf | ||
|
|
b1b01ed383 | ||
|
|
6f5d9dc41f | ||
|
|
ac190b89dd | ||
|
|
990441295c | ||
|
|
1e4c20073c | ||
|
|
80353d67a0 | ||
|
|
d55b4b051a | ||
|
|
25d90e3e12 | ||
|
|
1727eb5625 | ||
|
|
58f4224c66 | ||
|
|
5d491e0a12 | ||
|
|
1eef5a56c3 | ||
|
|
d6740503be | ||
|
|
9f647ead46 | ||
|
|
f480afcace | ||
|
|
8d73d15b8a | ||
|
|
e8370ef950 | ||
|
|
e859c8dc58 | ||
|
|
b4b38b9bc3 | ||
|
|
d32719ad8b | ||
|
|
99d0816d9f | ||
|
|
95f4b89fe8 | ||
|
|
3e63c2b717 | ||
|
|
42cd56578d | ||
|
|
31d4cb4eb7 | ||
|
|
fde3e8bea9 | ||
|
|
874ad29676 | ||
|
|
5503d9d432 | ||
|
|
43b3d68447 | ||
|
|
e3c747a867 | ||
|
|
e020a2c9a4 | ||
|
|
e6e6f09547 | ||
|
|
68ec946c59 | ||
|
|
5a6e5ce427 | ||
|
|
b2382211db | ||
|
|
12148506bd | ||
|
|
b5666fd4c3 | ||
|
|
bc7da563d8 | ||
|
|
d3acf4160d | ||
|
|
65e0c0e4c5 | ||
|
|
2dda6a509b | ||
|
|
7238414864 | ||
|
|
bab41ca8a6 | ||
|
|
376f023e0f | ||
|
|
0cbf525a78 | ||
|
|
f56fefec4d | ||
|
|
66eccd6dd8 | ||
|
|
c4b9a9e2f4 | ||
|
|
38921e58cd | ||
|
|
2c9c6c77f7 | ||
|
|
46605e049d | ||
|
|
01e1e82ced | ||
|
|
8e17716094 | ||
|
|
7c028b6479 | ||
|
|
cba4bae5cd | ||
|
|
55046f3017 | ||
|
|
4836621c31 | ||
|
|
6743aa4609 | ||
|
|
bbad0fbc18 | ||
|
|
8f6812ecb2 | ||
|
|
303b09f341 | ||
|
|
1f3ee41eb4 | ||
|
|
01c711a81e | ||
|
|
16bb5c3384 | ||
|
|
c55d97368e | ||
|
|
ef193aca31 | ||
|
|
c36a6797a9 | ||
|
|
f393fd4ad4 | ||
|
|
8987b9bdbc | ||
|
|
68bf426af0 | ||
|
|
1125900013 | ||
|
|
d272e823c3 | ||
|
|
3ea11d4f38 | ||
|
|
b4dc64594c | ||
|
|
6a763b3720 | ||
|
|
252ac1c704 | ||
|
|
c51e42a061 | ||
|
|
e3f9e477fe | ||
|
|
b265970b01 | ||
|
|
7b7b2ffc9a | ||
|
|
d019062709 | ||
|
|
ad9d728314 | ||
|
|
b564fb9a2f | ||
|
|
9c05f6839c | ||
|
|
4dea38f06d | ||
|
|
c4fc222204 | ||
|
|
00c3a2dd80 | ||
|
|
46fa0088f2 | ||
|
|
a19fd9b889 | ||
|
|
d379c10510 | ||
|
|
f3a58145ee | ||
|
|
e6125dc361 | ||
|
|
b78f718e9e | ||
|
|
53e87f7769 | ||
|
|
86eb40e0fd | ||
|
|
51caa6834a | ||
|
|
7a7a5b5285 | ||
|
|
f14ffb9e44 | ||
|
|
001b524005 | ||
|
|
a8cf4fb309 | ||
|
|
155775556d | ||
|
|
d66095bf9c | ||
|
|
0ddab30340 | ||
|
|
2a9e061575 | ||
|
|
351dce5cfd | ||
|
|
3255d9012c | ||
|
|
226dcb3fcf | ||
|
|
f80e9b2558 | ||
|
|
3f273235a6 | ||
|
|
e8554ed5c5 | ||
|
|
a03744008c | ||
|
|
fe8b13885c | ||
|
|
8995dcc82a | ||
|
|
e8e0d03265 | ||
|
|
56031f0358 | ||
|
|
1ef0f830d3 | ||
|
|
59e2f6e4a7 | ||
|
+15 |
67bb2ed798 | ||
|
|
ea4618a244 | ||
|
|
201a9d9356 | ||
|
|
40b0d57024 | ||
|
|
761f66fc5a | ||
|
|
2403d77166 | ||
|
|
b44f4d66f9 | ||
|
|
d178962c37 | ||
|
|
cf4d2b4ef5 | ||
|
|
3e4139ac23 | ||
|
|
9fb736f67a | ||
|
|
afc738141f | ||
|
|
efa195059e | ||
|
|
0f2b6bbe1a | ||
|
|
28b94a865f | ||
|
|
f45803fcfa | ||
|
|
053da1ec32 | ||
|
|
f0dcfc83c8 | ||
|
|
5eb3af54b3 | ||
|
|
3552bbc6cf | ||
|
|
028bd77c91 | ||
|
|
65a1aa6100 | ||
|
|
a09401c203 | ||
|
|
7c23773636 | ||
|
|
56ba8c0a07 | ||
|
|
5924bc0990 | ||
|
+27 |
10ffd9bacd | ||
|
|
20c45b5d88 | ||
|
|
3cab37ed8d | ||
|
|
98799b50f0 | ||
|
|
8071016848 | ||
|
|
aa5e2ac9a5 | ||
|
|
06857a4a69 | ||
|
|
b0d1720d92 | ||
|
|
e148ee7823 | ||
|
|
bb9a039931 | ||
|
|
053f93ea0a | ||
|
|
2a982c6b6d | ||
|
|
85490fef4d | ||
|
|
57afd7a2b1 | ||
|
|
0f9c3571fe |
@@ -1,6 +1,7 @@
|
||||
# ********** INDEX **********
|
||||
#
|
||||
# - APP STORE
|
||||
# - BASECAMP
|
||||
# - DAILY.CO VIDEO
|
||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||
# - HUBSPOT
|
||||
@@ -20,6 +21,14 @@
|
||||
|
||||
# - APP STORE **********************************************************************************************
|
||||
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
|
||||
|
||||
# - BASECAMP
|
||||
# Used to enable Basecamp integration with Cal.com
|
||||
# @see https://github.com/calcom/cal.com#obtaining-basecamp-client-id-and-secret
|
||||
BASECAMP3_CLIENT_ID=
|
||||
BASECAMP3_CLIENT_SECRET=
|
||||
BASECAMP3_USER_AGENT=
|
||||
|
||||
# - DAILY.CO VIDEO
|
||||
# Enables Cal Video. to get your key
|
||||
# 1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
|
||||
|
||||
+12
-2
@@ -126,8 +126,7 @@ TWILIO_WHATSAPP_PHONE_NUMBER=
|
||||
NEXT_PUBLIC_SENDER_ID=
|
||||
TWILIO_VERIFY_SID=
|
||||
|
||||
# This is used so we can bypass emails in auth flows for E2E testing
|
||||
# Set it to "1" if you need to run E2E tests locally
|
||||
# Set it to "1" if you need to run E2E tests locally.
|
||||
NEXT_PUBLIC_IS_E2E=
|
||||
|
||||
# Used for internal billing system
|
||||
@@ -172,6 +171,12 @@ EMAIL_SERVER_PORT=1025
|
||||
## You will need to provision an App Password.
|
||||
## @see https://support.google.com/accounts/answer/185833
|
||||
# EMAIL_SERVER_PASSWORD='<gmail_app_password>'
|
||||
|
||||
# Used for E2E for email testing
|
||||
# Set it to "1" if you need to email checks in E2E tests locally
|
||||
# Make sure to run mailhog container manually or with `yarn dx`
|
||||
E2E_TEST_MAILHOG_ENABLED=
|
||||
|
||||
# **********************************************************************************************************
|
||||
|
||||
# Set the following value to true if you wish to enable Team Impersonation
|
||||
@@ -215,3 +220,8 @@ PROJECT_ID_VERCEL=
|
||||
TEAM_ID_VERCEL=
|
||||
# Get it from: https://vercel.com/account/tokens
|
||||
AUTH_BEARER_TOKEN_VERCEL=
|
||||
|
||||
# - APPLE CALENDAR
|
||||
# Used for E2E tests on Apple Calendar
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL=""
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD=""
|
||||
|
||||
@@ -5,16 +5,15 @@ on:
|
||||
types:
|
||||
- opened
|
||||
|
||||
|
||||
jobs:
|
||||
label_on_pr:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
permissions:
|
||||
contents: none
|
||||
issues: read
|
||||
pull-requests: write
|
||||
|
||||
|
||||
steps:
|
||||
- name: Apply labels from linked issue to PR
|
||||
uses: actions/github-script@v5
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# .github/workflows/chromatic.yml
|
||||
|
||||
# Workflow name
|
||||
name: 'Chromatic'
|
||||
name: "Chromatic"
|
||||
|
||||
# Event for the workflow
|
||||
on:
|
||||
|
||||
@@ -4,8 +4,8 @@ on:
|
||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||
schedule:
|
||||
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
|
||||
- cron: "0,15,30,45 * * * *"
|
||||
# Runs "At every 15th minute." (see https://crontab.guru)
|
||||
- cron: "*/15 * * * *"
|
||||
jobs:
|
||||
cron-bookingReminder:
|
||||
env:
|
||||
@@ -20,4 +20,4 @@ jobs:
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
|
||||
--fail
|
||||
-sSf
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||
schedule:
|
||||
# Runs “Every month at 1st (see https://crontab.guru)
|
||||
# Runs "At 00:00 on day-of-month 1." (see https://crontab.guru)
|
||||
- cron: "0 0 1 * *"
|
||||
jobs:
|
||||
cron-downgradeUsers:
|
||||
@@ -21,4 +21,4 @@ jobs:
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
|
||||
--fail
|
||||
-sSf
|
||||
|
||||
@@ -4,8 +4,8 @@ on:
|
||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||
schedule:
|
||||
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
|
||||
- cron: "0,15,30,45 * * * *"
|
||||
# Runs "At every 15th minute." (see https://crontab.guru)
|
||||
- cron: "*/15 * * * *"
|
||||
jobs:
|
||||
cron-scheduleEmailReminders:
|
||||
env:
|
||||
@@ -20,4 +20,4 @@ jobs:
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
|
||||
--fail
|
||||
-sSf
|
||||
|
||||
@@ -4,8 +4,8 @@ on:
|
||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||
schedule:
|
||||
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
|
||||
- cron: "0,15,30,45 * * * *"
|
||||
# Runs "At every 15th minute." (see https://crontab.guru)
|
||||
- cron: "*/15 * * * *"
|
||||
jobs:
|
||||
cron-scheduleSMSReminders:
|
||||
env:
|
||||
@@ -20,4 +20,4 @@ jobs:
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
|
||||
--fail
|
||||
-sSf
|
||||
|
||||
@@ -4,8 +4,8 @@ on:
|
||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||
schedule:
|
||||
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
|
||||
- cron: "0,15,30,45 * * * *"
|
||||
# Runs "At every 15th minute." (see https://crontab.guru)
|
||||
- cron: "*/15 * * * *"
|
||||
jobs:
|
||||
cron-scheduleWhatsappReminders:
|
||||
env:
|
||||
@@ -20,4 +20,4 @@ jobs:
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
|
||||
--fail
|
||||
-sSf
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||
schedule:
|
||||
# Runs every day (see https://crontab.guru)
|
||||
# Runs "At 00:00." every day (see https://crontab.guru)
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||
schedule:
|
||||
# Runs “Every month at 1st (see https://crontab.guru)
|
||||
# Runs "At 00:00 on day-of-month 1." (see https://crontab.guru)
|
||||
- cron: "0 0 1 * *"
|
||||
jobs:
|
||||
cron-syncAppMeta:
|
||||
@@ -21,4 +21,4 @@ jobs:
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
|
||||
--fail
|
||||
-sSf
|
||||
|
||||
@@ -41,6 +41,9 @@ jobs:
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||
|
||||
@@ -38,6 +38,8 @@ jobs:
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||
|
||||
@@ -41,6 +41,9 @@ jobs:
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||
|
||||
@@ -40,11 +40,14 @@ jobs:
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
||||
EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
||||
EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
||||
EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD}}
|
||||
EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD}}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
|
||||
|
||||
@@ -7,6 +7,8 @@ env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||
|
||||
@@ -7,6 +7,8 @@ env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||
|
||||
@@ -8,7 +8,7 @@ on: # yamllint disable-line rule:truthy
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
RELEASE_TAG:
|
||||
description: 'v{Major}.{Minor}.{Patch}'
|
||||
description: "v{Major}.{Minor}.{Patch}"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: "Determine tag"
|
||||
run: "echo \"RELEASE_TAG=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV"
|
||||
run: 'echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV'
|
||||
|
||||
- name: "Run remote release workflow"
|
||||
uses: "actions/github-script@v6"
|
||||
|
||||
@@ -29,11 +29,11 @@ jobs:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Hey there and thank you for opening this pull request! 👋🏼
|
||||
|
||||
|
||||
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
||||
|
||||
Details:
|
||||
|
||||
|
||||
```
|
||||
${{ steps.lint_pr_title.outputs.error_message }}
|
||||
```
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
# Delete a previous comment when the issue has been resolved
|
||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Thank you for following the naming conventions! 🙏
|
||||
Thank you for following the naming conventions! 🙏
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: Submodule Sync
|
||||
on:
|
||||
schedule:
|
||||
# Runs "At minute 15 past every 4th hour." (see https://crontab.guru)
|
||||
- cron: "15 */4 * * *"
|
||||
workflow_dispatch: ~
|
||||
jobs:
|
||||
|
||||
+1
-1
@@ -40,4 +40,4 @@ vscode:
|
||||
- bradlc.vscode-tailwindcss
|
||||
- ban.spellright
|
||||
- stripe.vscode-stripe
|
||||
- Prisma.prisma
|
||||
- Prisma.prisma
|
||||
|
||||
@@ -221,6 +221,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||
```
|
||||
|
||||
1. Run [mailhog](https://github.com/mailhog/MailHog) to view emails sent during development
|
||||
> **_NOTE:_** Required when `E2E_TEST_MAILHOG_ENABLED` is "1"
|
||||
|
||||
```sh
|
||||
docker pull mailhog/mailhog
|
||||
@@ -461,6 +462,18 @@ following
|
||||
4. Now paste the API key to your `.env` file into the `DAILY_API_KEY` field in your `.env` file.
|
||||
5. If you have the [Daily Scale Plan](https://daily.co/pricing) set the `DAILY_SCALE_PLAN` variable to `true` in order to use features like video recording.
|
||||
|
||||
### Obtaining Basecamp Client ID and Secret
|
||||
|
||||
1. Visit the [37 Signals Integrations Dashboard](launchpad.37signals.com/integrations) and sign in.
|
||||
2. Register a new application by clicking the Register one now link.
|
||||
3. Fill in your company details.
|
||||
4. Select Basecamp 4 as the product to integrate with.
|
||||
5. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/basecamp3/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||
6. Click on done and copy the Client ID and secret into the `BASECAMP3_CLIENT_ID` and `BASECAMP3_CLIENT_SECRET` fields.
|
||||
7. Set the `BASECAMP3_CLIENT_SECRET` env variable to `{your_domain} ({support_email})`.
|
||||
For example, `Cal.com (support@cal.com)`.
|
||||
|
||||
|
||||
### Obtaining HubSpot Client ID and Secret
|
||||
|
||||
1. Open [HubSpot Developer](https://developer.hubspot.com/) and sign into your account, or create a new one.
|
||||
|
||||
@@ -33,12 +33,14 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
|
||||
position: true,
|
||||
eventName: true,
|
||||
timeZone: true,
|
||||
schedulingType: true,
|
||||
// START Limit future bookings
|
||||
periodType: true,
|
||||
periodStartDate: true,
|
||||
schedulingType: true,
|
||||
periodEndDate: true,
|
||||
periodDays: true,
|
||||
periodCountCalendarDays: true,
|
||||
// END Limit future bookings
|
||||
requiresConfirmation: true,
|
||||
disableGuests: true,
|
||||
hideCalendarNotes: true,
|
||||
@@ -51,6 +53,8 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
|
||||
slotInterval: true,
|
||||
successRedirectUrl: true,
|
||||
locations: true,
|
||||
bookingLimits: true,
|
||||
durationLimits: true,
|
||||
})
|
||||
.merge(z.object({ hosts: z.array(hostSchema).optional().default([]) }))
|
||||
.partial()
|
||||
@@ -67,6 +71,7 @@ const schemaEventTypeCreateParams = z
|
||||
seatsPerTimeSlot: z.number().optional(),
|
||||
seatsShowAttendees: z.boolean().optional(),
|
||||
bookingFields: eventTypeBookingFields.optional(),
|
||||
scheduleId: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -85,6 +90,7 @@ const schemaEventTypeEditParams = z
|
||||
seatsPerTimeSlot: z.number().optional(),
|
||||
seatsShowAttendees: z.boolean().optional(),
|
||||
bookingFields: eventTypeBookingFields.optional(),
|
||||
scheduleId: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -98,6 +104,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
|
||||
position: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
scheduleId: true,
|
||||
eventName: true,
|
||||
timeZone: true,
|
||||
periodType: true,
|
||||
@@ -123,6 +130,8 @@ export const schemaEventTypeReadPublic = EventType.pick({
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
bookingFields: true,
|
||||
bookingLimits: true,
|
||||
durationLimits: true,
|
||||
}).merge(
|
||||
z.object({
|
||||
locations: z
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { withValidation } from "next-validations";
|
||||
import { z } from "zod";
|
||||
|
||||
import { baseApiParams } from "./baseApiParams";
|
||||
|
||||
// Extracted out as utility function so can be reused
|
||||
// at different endpoints that require this validation.
|
||||
export const schemaQueryUserEmail = baseApiParams.extend({
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
export const schemaQuerySingleOrMultipleUserEmails = z.object({
|
||||
email: z.union([z.string().email(), z.array(z.string().email())]),
|
||||
});
|
||||
|
||||
export const withValidQueryUserEmail = withValidation({
|
||||
schema: schemaQueryUserEmail,
|
||||
type: "Zod",
|
||||
mode: "query",
|
||||
});
|
||||
@@ -29,7 +29,7 @@ enum locales {
|
||||
RO = "ro",
|
||||
NL = "nl",
|
||||
PT_BR = "pt-BR",
|
||||
ES_419 = "es-419",
|
||||
// ES_419 = "es-419", // Disabled until Crowdin reaches at least 80% completion
|
||||
KO = "ko",
|
||||
JA = "ja",
|
||||
PL = "pl",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
import { checkPermissions as canAccessTeamEventOrThrow } from "~/pages/api/teams/[teamId]/_auth-middleware";
|
||||
|
||||
import getCalLink from "../_utils/getCalLink";
|
||||
|
||||
@@ -42,7 +44,8 @@ import getCalLink from "../_utils/getCalLink";
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { prisma, query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const event_type = await prisma.eventType.findUnique({
|
||||
|
||||
const eventType = await prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
customInputs: true,
|
||||
@@ -51,10 +54,43 @@ export async function getHandler(req: NextApiRequest) {
|
||||
owner: { select: { username: true, id: true } },
|
||||
},
|
||||
});
|
||||
await checkPermissions(req, eventType);
|
||||
|
||||
const link = event_type ? getCalLink(event_type) : null;
|
||||
const link = eventType ? getCalLink(eventType) : null;
|
||||
// user.defaultScheduleId doesn't work the same for team events.
|
||||
if (!eventType?.scheduleId && eventType?.userId && !eventType?.teamId) {
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
where: {
|
||||
id: eventType.userId,
|
||||
},
|
||||
select: {
|
||||
defaultScheduleId: true,
|
||||
},
|
||||
});
|
||||
eventType.scheduleId = user.defaultScheduleId;
|
||||
}
|
||||
|
||||
return { event_type: schemaEventTypeReadPublic.parse({ ...event_type, link }) };
|
||||
// TODO: eventType when not found should be a 404
|
||||
// but API consumers may depend on the {} behaviour.
|
||||
return { event_type: schemaEventTypeReadPublic.parse({ ...eventType, link }) };
|
||||
}
|
||||
|
||||
type BaseEventTypeCheckPermissions = {
|
||||
userId: number | null;
|
||||
teamId: number | null;
|
||||
};
|
||||
|
||||
async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
|
||||
req: NextApiRequest,
|
||||
eventType: (T & Partial<Omit<T, keyof BaseEventTypeCheckPermissions>>) | null
|
||||
) {
|
||||
if (req.isAdmin) return true;
|
||||
if (eventType?.teamId) {
|
||||
req.query.teamId = String(eventType.teamId);
|
||||
await canAccessTeamEventOrThrow(req, "MEMBER");
|
||||
}
|
||||
if (eventType?.userId === req.userId) return true; // is owner.
|
||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
@@ -52,6 +52,9 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
|
||||
* slug:
|
||||
* type: string
|
||||
* description: Unique slug for the event type
|
||||
* scheduleId:
|
||||
* type: number
|
||||
* description: The ID of the schedule for this event type
|
||||
* hosts:
|
||||
* type: array
|
||||
* items:
|
||||
@@ -199,10 +202,17 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { prisma, query, body } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const { hosts = [], ...parsedBody } = schemaEventTypeEditBodyParams.parse(body);
|
||||
const {
|
||||
hosts = [],
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
...parsedBody
|
||||
} = schemaEventTypeEditBodyParams.parse(body);
|
||||
|
||||
const data: Prisma.EventTypeUpdateArgs["data"] = {
|
||||
...parsedBody,
|
||||
bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits,
|
||||
durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits,
|
||||
};
|
||||
|
||||
if (hosts) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import type { PrismaClient } from "@calcom/prisma/client";
|
||||
|
||||
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
@@ -35,20 +35,12 @@ import getCalLink from "./_utils/getCalLink";
|
||||
* description: No event types were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin, prisma } = req;
|
||||
|
||||
const args: Prisma.EventTypeFindManyArgs = {
|
||||
where: { userId },
|
||||
};
|
||||
/** Only admins can query other users */
|
||||
if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 401, message: "ADMIN required" });
|
||||
if (isAdmin && req.query.userId) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
args.where = { userId: { in: userIds } };
|
||||
}
|
||||
const { userId, prisma } = req;
|
||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||
const data = await prisma.eventType.findMany({
|
||||
...args,
|
||||
where: {
|
||||
userId: { in: userIds },
|
||||
},
|
||||
include: {
|
||||
customInputs: true,
|
||||
team: { select: { slug: true } },
|
||||
@@ -56,13 +48,74 @@ async function getHandler(req: NextApiRequest) {
|
||||
owner: { select: { username: true, id: true } },
|
||||
},
|
||||
});
|
||||
// this really should return [], but backwards compatibility..
|
||||
if (data.length === 0) new HttpError({ statusCode: 404, message: "No event types were found" });
|
||||
return {
|
||||
event_types: data.map((eventType) => {
|
||||
const link = getCalLink(eventType);
|
||||
return schemaEventTypeReadPublic.parse({ ...eventType, link });
|
||||
}),
|
||||
event_types: (await defaultScheduleId<(typeof data)[number]>({ eventTypes: data, prisma, userIds })).map(
|
||||
(eventType) => {
|
||||
const link = getCalLink(eventType);
|
||||
return schemaEventTypeReadPublic.parse({ ...eventType, link });
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
// TODO: Extract & reuse.
|
||||
function extractUserIdsFromQuery({ isAdmin, query }: NextApiRequest) {
|
||||
/** Guard: Only admins can query other users */
|
||||
if (!isAdmin) {
|
||||
throw new HttpError({ statusCode: 401, message: "ADMIN required" });
|
||||
}
|
||||
const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query);
|
||||
return Array.isArray(userIdOrUserIds) ? userIdOrUserIds : [userIdOrUserIds];
|
||||
}
|
||||
|
||||
type DefaultScheduleIdEventTypeBase = {
|
||||
scheduleId: number | null;
|
||||
userId: number | null;
|
||||
};
|
||||
// If an eventType is given w/o a scheduleId
|
||||
// Then we associate the default user schedule id to the eventType
|
||||
async function defaultScheduleId<T extends DefaultScheduleIdEventTypeBase>({
|
||||
prisma,
|
||||
eventTypes,
|
||||
userIds,
|
||||
}: {
|
||||
prisma: PrismaClient;
|
||||
eventTypes: (T & Partial<Omit<T, keyof DefaultScheduleIdEventTypeBase>>)[];
|
||||
userIds: number[];
|
||||
}) {
|
||||
// there is no event types without a scheduleId, skip the user query
|
||||
if (eventTypes.every((eventType) => eventType.scheduleId)) return eventTypes;
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: userIds,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
defaultScheduleId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!users.length) {
|
||||
return eventTypes;
|
||||
}
|
||||
|
||||
const defaultScheduleIds = users.reduce((result, user) => {
|
||||
result[user.id] = user.defaultScheduleId;
|
||||
return result;
|
||||
}, {} as { [x: number]: number | null });
|
||||
|
||||
return eventTypes.map((eventType) => {
|
||||
// realistically never happens, userId should't be null on personal event types.
|
||||
if (!eventType.userId) return eventType;
|
||||
return {
|
||||
...eventType,
|
||||
scheduleId: eventType.scheduleId || defaultScheduleIds[eventType.userId],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
@@ -60,6 +60,9 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
|
||||
* hidden:
|
||||
* type: boolean
|
||||
* description: If the event type should be hidden from your public booking page
|
||||
* scheduleId:
|
||||
* type: number
|
||||
* description: The ID of the schedule for this event type
|
||||
* position:
|
||||
* type: integer
|
||||
* description: The position of the event type on the public booking page
|
||||
@@ -178,6 +181,7 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
|
||||
* position: 0
|
||||
* eventName: null
|
||||
* timeZone: null
|
||||
* scheduleId: 5
|
||||
* periodType: UNLIMITED
|
||||
* periodStartDate: 2023-02-15T08:46:16.000Z
|
||||
* periodEndDate: 2023-0-15T08:46:16.000Z
|
||||
@@ -255,12 +259,19 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin, prisma, body } = req;
|
||||
|
||||
const { hosts = [], ...parsedBody } = schemaEventTypeCreateBodyParams.parse(body || {});
|
||||
const {
|
||||
hosts = [],
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
...parsedBody
|
||||
} = schemaEventTypeCreateBodyParams.parse(body || {});
|
||||
|
||||
let data: Prisma.EventTypeCreateArgs["data"] = {
|
||||
...parsedBody,
|
||||
userId,
|
||||
users: { connect: { id: userId } },
|
||||
bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits,
|
||||
durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits,
|
||||
};
|
||||
|
||||
await checkPermissions(req);
|
||||
|
||||
@@ -59,7 +59,7 @@ async function getHandler(req: NextApiRequest) {
|
||||
};
|
||||
|
||||
const data = await prisma.eventType.findMany(args);
|
||||
return { event_types: data.map((attendee) => schemaEventTypeReadPublic.parse(attendee)) };
|
||||
return { event_types: data.map((eventType) => schemaEventTypeReadPublic.parse(eventType)) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { NextApiRequest } from "next";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
import { schemaQuerySingleOrMultipleUserEmails } from "~/lib/validations/shared/queryUserEmail";
|
||||
import { schemaUsersReadPublic } from "~/lib/validations/user";
|
||||
|
||||
/**
|
||||
@@ -19,6 +20,17 @@ import { schemaUsersReadPublic } from "~/lib/validations/user";
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: query
|
||||
* name: email
|
||||
* required: false
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* format: email
|
||||
* style: form
|
||||
* explode: true
|
||||
* description: The email address or an array of email addresses to filter by
|
||||
* tags:
|
||||
* - users
|
||||
* responses:
|
||||
@@ -39,6 +51,14 @@ export async function getHandler(req: NextApiRequest) {
|
||||
const where: Prisma.UserWhereInput = {};
|
||||
// If user is not ADMIN, return only his data.
|
||||
if (!isAdmin) where.id = userId;
|
||||
|
||||
if (req.query.email) {
|
||||
const validationResult = schemaQuerySingleOrMultipleUserEmails.parse(req.query);
|
||||
where.email = {
|
||||
in: Array.isArray(validationResult.email) ? validationResult.email : [validationResult.email],
|
||||
};
|
||||
}
|
||||
|
||||
const [total, data] = await prisma.$transaction([
|
||||
prisma.user.count({ where }),
|
||||
prisma.user.findMany({ where, take, skip }),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Set the version of docker compose to use
|
||||
version: '3.9'
|
||||
version: "3.9"
|
||||
|
||||
# The containers that compose the project
|
||||
services:
|
||||
@@ -8,8 +8,8 @@ services:
|
||||
restart: always
|
||||
container_name: integration-tests-prisma
|
||||
ports:
|
||||
- '5433:5432'
|
||||
- "5433:5432"
|
||||
environment:
|
||||
POSTGRES_USER: prisma
|
||||
POSTGRES_PASSWORD: prisma
|
||||
POSTGRES_DB: tests
|
||||
POSTGRES_DB: tests
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
const ns = ["common"];
|
||||
const supportedLngs = ["en", "fr"];
|
||||
const resources = ns.reduce((acc, n) => {
|
||||
supportedLngs.forEach((lng) => {
|
||||
if (!acc[lng]) acc[lng] = {};
|
||||
acc[lng] = {
|
||||
...acc[lng],
|
||||
[n]: require(`../../web/public/static/locales/${lng}/${n}.json`),
|
||||
};
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: [],
|
||||
debug: true,
|
||||
fallbackLng: "en",
|
||||
defaultNS: "common",
|
||||
ns,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
react: { useSuspense: true },
|
||||
resources,
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -15,6 +15,7 @@ module.exports = {
|
||||
"storybook-addon-rtl-direction",
|
||||
"storybook-react-i18next",
|
||||
"storybook-addon-next",
|
||||
"storybook-addon-next-router",
|
||||
/*{
|
||||
name: "storybook-addon-next",
|
||||
options: {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script>
|
||||
window.global = window;
|
||||
window.isEmbed = ()=> {
|
||||
return location.search.includes("embed=")
|
||||
}
|
||||
window.isEmbed = () => {
|
||||
return location.search.includes("embed=");
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500&display=swap');
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@500&display=swap");
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { addDecorator } from "@storybook/react";
|
||||
import { AppRouterContext } from "next/dist/shared/lib/app-router-context";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
|
||||
import "../styles/globals.css";
|
||||
@@ -13,6 +14,21 @@ export const parameters = {
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
nextRouter: {
|
||||
pathname: "/",
|
||||
asPath: "/",
|
||||
query: {},
|
||||
push() {},
|
||||
Provider: AppRouterContext.Provider,
|
||||
},
|
||||
globals: {
|
||||
locale: "en",
|
||||
locales: {
|
||||
en: "English",
|
||||
fr: "Français",
|
||||
},
|
||||
},
|
||||
i18n,
|
||||
};
|
||||
|
||||
addDecorator((storyFn) => (
|
||||
|
||||
@@ -18,6 +18,7 @@ export function VariantsTable({
|
||||
const columns = React.Children.toArray(children) as ReactElement<RowProps>[];
|
||||
return (
|
||||
<div
|
||||
id="light-variant"
|
||||
className={classNames(
|
||||
isDark &&
|
||||
"relative py-8 before:absolute before:left-0 before:top-0 before:block before:h-full before:w-screen before:bg-[#1C1C1C]"
|
||||
@@ -43,7 +44,7 @@ export function VariantsTable({
|
||||
</table>
|
||||
</div>
|
||||
{!isDark && (
|
||||
<div data-mode="dark" className="dark">
|
||||
<div id="dark-variant" data-mode="dark" className="dark">
|
||||
<VariantsTable titles={titles} isDark columnMinWidth={columnMinWidth}>
|
||||
{children}
|
||||
</VariantsTable>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
import { Meta } from "@storybook/addon-docs";
|
||||
|
||||
<Meta title="Introduction" />
|
||||
|
||||
<div className="text-center flex flex-col items-center">
|
||||
<h1 style={{marginBottom: '24px', marginTop: '36px'}}>Welcome to Cal.com UI</h1>
|
||||
<p>This is the beginning of our storybook improvements to match Figma as close as possible. Like our Figma library, we will be adding more components as we go along.</p>
|
||||
<p>Our <a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">Figma</a> library is avalible for anyone to view and use. If you have any questions or concerns, please reach out to the design team.</p>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<h1 style={{ marginBottom: "24px", marginTop: "36px" }}>Welcome to Cal.com UI</h1>
|
||||
<p>
|
||||
This is the beginning of our storybook improvements to match Figma as close as possible. Like our Figma
|
||||
library, we will be adding more components as we go along.
|
||||
</p>
|
||||
<p>
|
||||
Our{" "}
|
||||
<a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">
|
||||
Figma
|
||||
</a>{" "}
|
||||
library is avalible for anyone to view and use. If you have any questions or concerns, please reach out to
|
||||
the design team.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<img src="/sb-cover.jpg" />
|
||||
<img src="/sb-cover.jpg" />
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"next": "^13.4.6",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"storybook-addon-next-router": "^4.0.2",
|
||||
"storybook-addon-rtl-direction": "^0.0.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@import url("../../../packages/features/calendars/weeklyview/styles/styles.css");
|
||||
|
||||
.sbdocs {
|
||||
font-family: 'Inter var' !important;
|
||||
padding: 0!important;
|
||||
font-family: "Inter var" !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#docs-root {
|
||||
@@ -21,8 +21,8 @@
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-top: 2px solid rgba(0, 0, 0, 0.12);
|
||||
padding-top: 12px!important;
|
||||
padding-bottom: 12px!important;
|
||||
padding-top: 12px !important;
|
||||
padding-bottom: 12px !important;
|
||||
margin: 82px 0 0 0;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
|
||||
/** Docs table **/
|
||||
.custom-args-wrapper{
|
||||
.custom-args-wrapper {
|
||||
max-height: 400px;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
@@ -45,7 +45,7 @@
|
||||
}
|
||||
|
||||
.docblock-argstable-body {
|
||||
box-shadow: none!important;
|
||||
box-shadow: none !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
|
||||
.docblock-argstable-body div p,
|
||||
.docblock-argstable-body div span {
|
||||
color: #8F8F8F !important;
|
||||
color: #8f8f8f !important;
|
||||
}
|
||||
|
||||
/** Custom components **/
|
||||
@@ -83,8 +83,8 @@
|
||||
}
|
||||
|
||||
.story-title h1 span {
|
||||
color: #9CA3AF;
|
||||
font-family: 'Inter var';
|
||||
color: #9ca3af;
|
||||
font-family: "Inter var";
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
@@ -96,7 +96,7 @@
|
||||
}
|
||||
|
||||
.examples {
|
||||
background-color: #F9FAFB;
|
||||
background-color: #f9fafb;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -111,14 +111,14 @@
|
||||
}
|
||||
|
||||
.examples-title {
|
||||
color: #8F8F8F;
|
||||
color: #8f8f8f;
|
||||
font-size: 14px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.examples-item-title {
|
||||
font-size: 12px;
|
||||
color: #8F8F8F;
|
||||
color: #8f8f8f;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
@@ -130,7 +130,7 @@
|
||||
.examples-footnote p,
|
||||
.examples-footnote ul,
|
||||
.examples-footnote li {
|
||||
color: #8F8F8F;
|
||||
color: #8f8f8f;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -139,11 +139,11 @@
|
||||
}
|
||||
|
||||
.examples-footnote li {
|
||||
margin: 0!important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.story-note {
|
||||
background-color: #F9FAFB;
|
||||
background-color: #f9fafb;
|
||||
font-size: 14px;
|
||||
padding: 20px;
|
||||
margin-bottom: 12px;
|
||||
@@ -161,12 +161,12 @@
|
||||
box-shadow: none !important;
|
||||
margin: 0 !important;
|
||||
border: none !important;
|
||||
border-radius: none!important;
|
||||
border-radius: none !important;
|
||||
}
|
||||
|
||||
.docs-story > div:first-child {
|
||||
padding: 0!important;
|
||||
margin: 0!important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.docs-story .innerZoomElementWrapper > div {
|
||||
@@ -174,7 +174,7 @@
|
||||
}
|
||||
|
||||
.sb-main-padded {
|
||||
padding: 0!important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
@@ -198,74 +198,72 @@
|
||||
:root {
|
||||
/* background */
|
||||
|
||||
--cal-bg-emphasis: #E5E7EB;
|
||||
--cal-bg-emphasis: #e5e7eb;
|
||||
--cal-bg: white;
|
||||
--cal-bg-subtle: #F3F4F6;
|
||||
--cal-bg-muted: #F9FAFB;
|
||||
--cal-bg-subtle: #f3f4f6;
|
||||
--cal-bg-muted: #f9fafb;
|
||||
--cal-bg-inverted: #111827;
|
||||
|
||||
/* background -> components*/
|
||||
--cal-bg-info: #DEE9FC;
|
||||
--cal-bg-success: #E2FBE8;
|
||||
--cal-bg-attention: #FCEED8;
|
||||
--cal-bg-error: #F9E3E2;
|
||||
--cal-bg-info: #dee9fc;
|
||||
--cal-bg-success: #e2fbe8;
|
||||
--cal-bg-attention: #fceed8;
|
||||
--cal-bg-error: #f9e3e2;
|
||||
--cal-bg-dark-error: #752522;
|
||||
|
||||
|
||||
/* Borders */
|
||||
--cal-border-emphasis:#9CA3AF;
|
||||
--cal-border: #D1D5DB;
|
||||
--cal-border-subtle:#E5E7EB;
|
||||
--cal-border-muted:#F3F4F6;
|
||||
--cal-border-emphasis: #9ca3af;
|
||||
--cal-border: #d1d5db;
|
||||
--cal-border-subtle: #e5e7eb;
|
||||
--cal-border-muted: #f3f4f6;
|
||||
--cal-border-error: #aa2e26;
|
||||
|
||||
/* Content/Text */
|
||||
--cal-text-emphasis: #111827;
|
||||
--cal-text:#374151;
|
||||
--cal-text-subtle:#6B7280;
|
||||
--cal-text-muted:#9CA3AF;
|
||||
--cal-text-inverted:white;
|
||||
--cal-text: #374151;
|
||||
--cal-text-subtle: #6b7280;
|
||||
--cal-text-muted: #9ca3af;
|
||||
--cal-text-inverted: white;
|
||||
|
||||
/* Content/Text -> components */
|
||||
--cal-text-info:#253985;
|
||||
--cal-text-success:#285231;
|
||||
--cal-text-attention:#73321B;
|
||||
--cal-text-error:#752522;
|
||||
--cal-text-info: #253985;
|
||||
--cal-text-success: #285231;
|
||||
--cal-text-attention: #73321b;
|
||||
--cal-text-error: #752522;
|
||||
|
||||
/* Brand shinanigans
|
||||
-> These will be computed for the users theme at runtime.
|
||||
*/
|
||||
--cal-brand:#111827;
|
||||
--cal-brand-emphasis:#101010;
|
||||
--cal-brand: #111827;
|
||||
--cal-brand-emphasis: #101010;
|
||||
--cal-brand-text: white;
|
||||
}
|
||||
.dark {
|
||||
/* background */
|
||||
|
||||
--cal-bg-emphasis: #2B2B2B;
|
||||
--cal-bg-emphasis: #2b2b2b;
|
||||
--cal-bg: #101010;
|
||||
--cal-bg-subtle: #2B2B2B;
|
||||
--cal-bg-muted: #1C1C1C;
|
||||
--cal-bg-inverted: #F3F4F6;
|
||||
--cal-bg-subtle: #2b2b2b;
|
||||
--cal-bg-muted: #1c1c1c;
|
||||
--cal-bg-inverted: #f3f4f6;
|
||||
|
||||
/* background -> components*/
|
||||
--cal-bg-info: #DEE9FC;
|
||||
--cal-bg-success: #E2FBE8;
|
||||
--cal-bg-attention: #FCEED8;
|
||||
--cal-bg-error: #F9E3E2;
|
||||
--cal-bg-info: #dee9fc;
|
||||
--cal-bg-success: #e2fbe8;
|
||||
--cal-bg-attention: #fceed8;
|
||||
--cal-bg-error: #f9e3e2;
|
||||
--cal-bg-dark-error: #752522;
|
||||
|
||||
|
||||
/* Borders */
|
||||
--cal-border-emphasis: #575757;
|
||||
--cal-border: #444444;
|
||||
--cal-border-subtle: #2B2B2B;
|
||||
--cal-border-muted: #1C1C1C;
|
||||
--cal-border-subtle: #2b2b2b;
|
||||
--cal-border-muted: #1c1c1c;
|
||||
--cal-border-error: #aa2e26;
|
||||
|
||||
/* Content/Text */
|
||||
--cal-text-emphasis: #F3F4F6;
|
||||
--cal-text: #D6D6D6;
|
||||
--cal-text-emphasis: #f3f4f6;
|
||||
--cal-text: #d6d6d6;
|
||||
--cal-text-subtle: #757575;
|
||||
--cal-text-muted: #575757;
|
||||
--cal-text-inverted: #101010;
|
||||
@@ -273,7 +271,7 @@
|
||||
/* Content/Text -> components */
|
||||
--cal-text-info: #253985;
|
||||
--cal-text-success: #285231;
|
||||
--cal-text-attention: #73321B;
|
||||
--cal-text-attention: #73321b;
|
||||
--cal-text-error: #752522;
|
||||
|
||||
/* Brand shenanigans
|
||||
|
||||
@@ -2,8 +2,8 @@ html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans,
|
||||
Droid Sans, Helvetica Neue, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -21,7 +21,7 @@ a {
|
||||
}
|
||||
.swagger-ui .opblock .opblock-summary {
|
||||
display: grid;
|
||||
flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
.opblock-summary-path {
|
||||
flex-shrink: 0;
|
||||
@@ -43,22 +43,22 @@ a {
|
||||
font-size: 22px;
|
||||
}
|
||||
.swagger-ui .scheme-container {
|
||||
padding: 14px 0;
|
||||
padding: 14px 0;
|
||||
}
|
||||
.swagger-ui .info {
|
||||
margin: 10px 0;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.swagger-ui .auth-wrapper {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.swagger-ui .authorization__btn {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
.swagger-ui .opblock {
|
||||
margin: 0 0 5px;
|
||||
}
|
||||
button.opblock-summary-control > svg {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
.swagger-ui .filter .operation-filter-input {
|
||||
border: 2px solid #d8dde7;
|
||||
@@ -73,15 +73,16 @@ a {
|
||||
.swagger-ui .info .title small {
|
||||
top: 5px;
|
||||
}
|
||||
.swagger-ui a.nostyle, .swagger-ui a.nostyle:visited {
|
||||
.swagger-ui a.nostyle,
|
||||
.swagger-ui a.nostyle:visited {
|
||||
width: 100%;
|
||||
}
|
||||
div.request-snippets > div.curl-command > div:nth-child(1) {
|
||||
overscroll-behavior: contain;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.swagger-ui .opblock-body pre.microlight {
|
||||
font-size: 9px;
|
||||
.swagger-ui .opblock-body pre.microlight {
|
||||
font-size: 9px;
|
||||
}
|
||||
.swagger-ui table tbody tr td {
|
||||
padding: 0px 0 0;
|
||||
@@ -91,7 +92,7 @@ a {
|
||||
font-size: 12px;
|
||||
}
|
||||
div.no-margin > div > div.responses-wrapper > div.responses-inner > div > div > table > tbody > tr {
|
||||
display: flex;
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
flex-direction: column;
|
||||
font-size: 60%;
|
||||
@@ -99,4 +100,4 @@ a {
|
||||
div.no-margin > div > div.responses-wrapper > div.responses-inner > div > div > table > thead > tr {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,51 @@
|
||||
import { lookup } from "bcp-47-match";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { CALCOM_VERSION } from "@calcom/lib/constants";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
export function useViewerI18n() {
|
||||
return trpc.viewer.public.i18n.useQuery(undefined, {
|
||||
staleTime: Infinity,
|
||||
/**
|
||||
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
|
||||
* We intend to not cache i18n query
|
||||
**/
|
||||
trpc: {
|
||||
context: { skipBatch: true },
|
||||
},
|
||||
});
|
||||
function useViewerI18n(locale: string) {
|
||||
return trpc.viewer.public.i18n.useQuery(
|
||||
{ locale, CalComVersion: CALCOM_VERSION },
|
||||
{
|
||||
/**
|
||||
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
|
||||
**/
|
||||
trpc: {
|
||||
context: { skipBatch: true },
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function useClientLocale(locales: string[]) {
|
||||
const session = useSession();
|
||||
// If the user is logged in, use their locale
|
||||
if (session.data?.user.locale) return session.data.user.locale;
|
||||
// If the user is not logged in, use the browser locale
|
||||
if (typeof window !== "undefined") {
|
||||
// This is the only way I found to ensure the prefetched locale is used on first render
|
||||
// FIXME: Find a better way to pick the best matching locale from the browser
|
||||
return lookup(locales, window.navigator.language) || window.navigator.language;
|
||||
}
|
||||
// If the browser is not available, use English
|
||||
return "en";
|
||||
}
|
||||
|
||||
export function useClientViewerI18n(locales: string[]) {
|
||||
const clientLocale = useClientLocale(locales);
|
||||
return useViewerI18n(clientLocale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-switches locale client-side to the logged in user's preference
|
||||
*/
|
||||
const I18nLanguageHandler = () => {
|
||||
const I18nLanguageHandler = (props: { locales: string[] }) => {
|
||||
const { locales } = props;
|
||||
const { i18n } = useTranslation("common");
|
||||
const locale = useViewerI18n().data?.locale || i18n.language;
|
||||
const locale = useClientViewerI18n(locales).data?.locale || i18n.language;
|
||||
|
||||
useEffect(() => {
|
||||
// bail early when i18n = {}
|
||||
|
||||
@@ -6,7 +6,7 @@ import Script from "next/script";
|
||||
|
||||
import "@calcom/embed-core/src/embed-iframe";
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import { WEBAPP_URL, IS_CALCOM } from "@calcom/lib/constants";
|
||||
import { IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||
|
||||
import type { AppProps } from "@lib/app-providers";
|
||||
@@ -72,7 +72,7 @@ function PageWrapper(props: AppProps) {
|
||||
}
|
||||
{...seoConfig.defaultNextSeo}
|
||||
/>
|
||||
<I18nLanguageHandler />
|
||||
<I18nLanguageHandler locales={props.router.locales || []} />
|
||||
<Script
|
||||
nonce={nonce}
|
||||
id="page-status"
|
||||
|
||||
@@ -336,7 +336,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<tr className="hover:bg-muted group flex flex-col sm:flex-row">
|
||||
<tr data-testid="booking-item" className="hover:bg-muted group flex flex-col sm:flex-row">
|
||||
<td
|
||||
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
||||
onClick={onClickTableData}>
|
||||
@@ -368,7 +368,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
{t("error_collecting_card")}
|
||||
</Badge>
|
||||
) : booking.paid ? (
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="green" data-testid="paid_badge">
|
||||
{booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
|
||||
</Badge>
|
||||
) : null}
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
Tooltip,
|
||||
} from "@calcom/ui";
|
||||
import { Copy, Edit } from "@calcom/ui/components/icon";
|
||||
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
|
||||
|
||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||
|
||||
@@ -286,36 +287,38 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
}}>
|
||||
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
|
||||
<div className="lg:-ml-2">
|
||||
<TextField
|
||||
disabled
|
||||
name="hashedLink"
|
||||
label={t("private_link_label")}
|
||||
data-testid="generated-hash-url"
|
||||
labelSrOnly
|
||||
type="text"
|
||||
hint={t("private_link_hint")}
|
||||
defaultValue={placeholderHashedLink}
|
||||
addOnSuffix={
|
||||
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
||||
<Button
|
||||
color="minimal"
|
||||
size="sm"
|
||||
type="button"
|
||||
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||
aria-label="copy link"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
if (eventType.hashedLink) {
|
||||
showToast(t("private_link_copied"), "success");
|
||||
} else {
|
||||
showToast(t("enabled_after_update_description"), "warning");
|
||||
}
|
||||
}}>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
{!IS_VISUAL_REGRESSION_TESTING && (
|
||||
<TextField
|
||||
disabled
|
||||
name="hashedLink"
|
||||
label={t("private_link_label")}
|
||||
data-testid="generated-hash-url"
|
||||
labelSrOnly
|
||||
type="text"
|
||||
hint={t("private_link_hint")}
|
||||
defaultValue={placeholderHashedLink}
|
||||
addOnSuffix={
|
||||
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
||||
<Button
|
||||
color="minimal"
|
||||
size="sm"
|
||||
type="button"
|
||||
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||
aria-label="copy link"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
if (eventType.hashedLink) {
|
||||
showToast(t("private_link_copied"), "success");
|
||||
} else {
|
||||
showToast(t("enabled_after_update_description"), "warning");
|
||||
}
|
||||
}}>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
<hr className="border-subtle" />
|
||||
@@ -338,6 +341,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
toggleGuests(false);
|
||||
formMethods.setValue("requiresConfirmation", false);
|
||||
setRequiresConfirmation(false);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
||||
} else {
|
||||
formMethods.setValue("seatsPerTimeSlot", null);
|
||||
|
||||
@@ -138,7 +138,7 @@ const EventTypeScheduleDetails = memo(
|
||||
<Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
|
||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||
</span>
|
||||
{!!schedule?.id && !schedule.isManaged && (
|
||||
{!!schedule?.id && !schedule.isManaged && !schedule.readOnly && (
|
||||
<Button
|
||||
href={`/availability/${schedule.id}`}
|
||||
disabled={isLoading}
|
||||
@@ -202,6 +202,15 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
isManaged: false,
|
||||
});
|
||||
}
|
||||
// We push the selected schedule from the event type if it's not part of the list response. This happens if the user is an admin but not the schedule owner.
|
||||
else if (eventType.schedule && !schedules.find((schedule) => schedule.id === eventType.schedule)) {
|
||||
options.push({
|
||||
value: eventType.schedule,
|
||||
label: eventType.scheduleName ?? t("default_schedule_name"),
|
||||
isDefault: false,
|
||||
isManaged: false,
|
||||
});
|
||||
}
|
||||
|
||||
setOptions(options);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { z } from "zod";
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import cx from "@calcom/lib/classNames";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -116,6 +117,8 @@ export const EventSetupTab = (
|
||||
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
||||
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
||||
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration);
|
||||
const orgBranding = useOrgBranding();
|
||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||
|
||||
const locationOptions = props.locationOptions.map((locationOption) => {
|
||||
const options = locationOption.options.filter((option) => {
|
||||
@@ -129,10 +132,12 @@ export const EventSetupTab = (
|
||||
};
|
||||
});
|
||||
|
||||
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
|
||||
value: mins,
|
||||
label: t("multiple_duration_mins", { count: mins }),
|
||||
}));
|
||||
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 150, 180].map(
|
||||
(mins) => ({
|
||||
value: mins,
|
||||
label: t("multiple_duration_mins", { count: mins }),
|
||||
})
|
||||
);
|
||||
|
||||
const [selectedMultipleDuration, setSelectedMultipleDuration] = useState<
|
||||
MultiValue<{
|
||||
@@ -383,6 +388,9 @@ export const EventSetupTab = (
|
||||
|
||||
const lengthLockedProps = shouldLockDisableProps("length");
|
||||
const descriptionLockedProps = shouldLockDisableProps("description");
|
||||
const urlPrefix = orgBranding
|
||||
? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
|
||||
: `${CAL_URL?.replace(/^(https?:|)\/\//, "")}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -411,10 +419,10 @@ export const EventSetupTab = (
|
||||
defaultValue={eventType.slug}
|
||||
addOnLeading={
|
||||
<>
|
||||
{CAL_URL?.replace(/^(https?:|)\/\//, "")}/
|
||||
{urlPrefix}/
|
||||
{!isManagedEventType
|
||||
? team
|
||||
? "team/" + team.slug
|
||||
? (orgBranding ? "" : "team/") + team.slug
|
||||
: eventType.users[0].username
|
||||
: t("username_placeholder")}
|
||||
/
|
||||
@@ -501,6 +509,8 @@ export const EventSetupTab = (
|
||||
<SettingsToggle
|
||||
title={t("allow_booker_to_select_duration")}
|
||||
checked={multipleDuration !== undefined}
|
||||
disabled={seatsEnabled}
|
||||
tooltip={seatsEnabled ? t("seat_options_doesnt_multiple_durations") : undefined}
|
||||
onCheckedChange={() => {
|
||||
if (multipleDuration !== undefined) {
|
||||
setMultipleDuration(undefined);
|
||||
|
||||
@@ -20,10 +20,11 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
|
||||
const { t } = useLocale();
|
||||
const { nextStep } = props;
|
||||
|
||||
const scheduleId = defaultScheduleId === null ? undefined : defaultScheduleId;
|
||||
const queryAvailability = trpc.viewer.availability.schedule.get.useQuery(
|
||||
{ scheduleId: defaultScheduleId! },
|
||||
{ scheduleId: defaultScheduleId ?? undefined },
|
||||
{
|
||||
enabled: !!defaultScheduleId,
|
||||
enabled: !!scheduleId,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -12,9 +12,6 @@ const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonati
|
||||
showToast(t("your_user_profile_updated_successfully"), "success");
|
||||
await utils.viewer.me.invalidate();
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.viewer.public.i18n.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { noop } from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Controller, FormProvider, useForm, useFormState } from "react-hook-form";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import classNames from "classnames";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { debounce, noop } from "lodash";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
@@ -50,7 +51,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
const { data: session, update } = useSession();
|
||||
const { update } = useSession();
|
||||
const {
|
||||
currentUsername,
|
||||
setCurrentUsername = noop,
|
||||
@@ -94,7 +95,6 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
debouncedApiCall(inputUsernameValue);
|
||||
}, [debouncedApiCall, inputUsernameValue]);
|
||||
|
||||
const utils = trpc.useContext();
|
||||
const updateUsername = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async () => {
|
||||
onSuccessMutation && (await onSuccessMutation());
|
||||
@@ -104,9 +104,6 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
onError: (error) => {
|
||||
onErrorMutation && onErrorMutation(error);
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.viewer.public.i18n.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
// when current username isn't set - Go to stripe to check what username he wanted to buy and was it a premium and was it paid for
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import classNames from "classnames";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { debounce, noop } from "lodash";
|
||||
import { useSession } from "next-auth/react";
|
||||
import type { RefCallback } from "react";
|
||||
@@ -24,7 +25,7 @@ interface ICustomUsernameProps {
|
||||
|
||||
const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.ComponentProps<typeof TextField>>) => {
|
||||
const { t } = useLocale();
|
||||
const { data: session, update } = useSession();
|
||||
const { update } = useSession();
|
||||
|
||||
const {
|
||||
currentUsername,
|
||||
@@ -65,8 +66,6 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
||||
}
|
||||
}, [inputUsernameValue, debouncedApiCall, currentUsername]);
|
||||
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const updateUsernameMutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async () => {
|
||||
onSuccessMutation && (await onSuccessMutation());
|
||||
@@ -77,9 +76,6 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
||||
onError: (error) => {
|
||||
onErrorMutation && onErrorMutation(error);
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.viewer.public.i18n.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const ActionButtons = () => {
|
||||
|
||||
@@ -1,35 +1,26 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
|
||||
import { PremiumTextfield } from "./PremiumTextfield";
|
||||
import { UsernameTextfield } from "./UsernameTextfield";
|
||||
|
||||
export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : PremiumTextfield;
|
||||
|
||||
interface UsernameAvailabilityFieldProps {
|
||||
onSuccessMutation?: () => void;
|
||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||
}
|
||||
|
||||
function useUserNamePrefix(organization: RouterOutputs["viewer"]["me"]["organization"]): string {
|
||||
return organization
|
||||
? organization.slug
|
||||
? getOrgFullDomain(organization.slug, { protocol: false })
|
||||
: organization.metadata && organization.metadata.requestedSlug
|
||||
? getOrgFullDomain(organization.metadata.requestedSlug, { protocol: false })
|
||||
: process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")
|
||||
: process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "");
|
||||
}
|
||||
export const getUsernameAvailabilityComponent = (isPremium: boolean) => {
|
||||
if (isPremium)
|
||||
return dynamic(() => import("./PremiumTextfield").then((m) => m.PremiumTextfield), { ssr: false });
|
||||
return dynamic(() => import("./UsernameTextfield").then((m) => m.UsernameTextfield), { ssr: false });
|
||||
};
|
||||
|
||||
export const UsernameAvailabilityField = ({
|
||||
onSuccessMutation,
|
||||
@@ -49,7 +40,12 @@ export const UsernameAvailabilityField = ({
|
||||
},
|
||||
});
|
||||
|
||||
const usernamePrefix = useUserNamePrefix(user.organization);
|
||||
const UsernameAvailability = getUsernameAvailabilityComponent(!IS_SELF_HOSTED && !user.organization?.id);
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
const usernamePrefix = orgBranding
|
||||
? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
|
||||
: `${CAL_URL?.replace(/^(https?:|)\/\//, "")}`;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
@@ -65,7 +61,8 @@ export const UsernameAvailabilityField = ({
|
||||
setInputUsernameValue={onChange}
|
||||
onSuccessMutation={onSuccessMutation}
|
||||
onErrorMutation={onErrorMutation}
|
||||
addOnLeading={usernamePrefix}
|
||||
disabled={!!user.organization?.id}
|
||||
addOnLeading={`${usernamePrefix}/`}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const IS_VISUAL_REGRESSION_TESTING = Boolean(globalThis.window?.Meticulous?.isRunningAsTest);
|
||||
@@ -1,7 +1,6 @@
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import type { Session } from "next-auth";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { SessionProvider, useSession } from "next-auth/react";
|
||||
import { EventCollectionProvider } from "next-collect/client";
|
||||
import type { SSRConfig } from "next-i18next";
|
||||
import { appWithTranslation } from "next-i18next";
|
||||
@@ -21,7 +20,7 @@ import { MetaProvider } from "@calcom/ui";
|
||||
import useIsBookingPage from "@lib/hooks/useIsBookingPage";
|
||||
import type { WithNonceProps } from "@lib/withNonce";
|
||||
|
||||
import { useViewerI18n } from "@components/I18nLanguageHandler";
|
||||
import { useClientViewerI18n } from "@components/I18nLanguageHandler";
|
||||
|
||||
const I18nextAdapter = appWithTranslation<
|
||||
NextJsAppProps<SSRConfig> & {
|
||||
@@ -69,11 +68,9 @@ type AppPropsWithoutNonce = Omit<AppPropsWithChildren, "pageProps"> & {
|
||||
const CustomI18nextProvider = (props: AppPropsWithoutNonce) => {
|
||||
/**
|
||||
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
|
||||
* We intend to not cache i18n query
|
||||
**/
|
||||
const { i18n, locale } = useViewerI18n().data ?? {
|
||||
locale: "en",
|
||||
};
|
||||
const clientViewerI18n = useClientViewerI18n(props.router.locales || []);
|
||||
const { i18n, locale } = clientViewerI18n.data || {};
|
||||
|
||||
const passedProps = {
|
||||
...props,
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
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";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { i18n } = require("@calcom/config/next-i18next.config");
|
||||
|
||||
export function getLocaleFromHeaders(req: IncomingMessage): string {
|
||||
let preferredLocale: string | null | undefined;
|
||||
if (req.headers["accept-language"]) {
|
||||
preferredLocale = parser.pick(i18n.locales, req.headers["accept-language"]) as Maybe<string>;
|
||||
}
|
||||
return preferredLocale ?? i18n.defaultLocale;
|
||||
}
|
||||
|
||||
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) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
},
|
||||
select: {
|
||||
locale: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (user?.locale) {
|
||||
return user.locale;
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
},
|
||||
data: {
|
||||
locale: preferredLocale,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return preferredLocale;
|
||||
};
|
||||
@@ -3,6 +3,7 @@ const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const os = require("os");
|
||||
const englishTranslation = require("./public/static/locales/en/common.json");
|
||||
const { withAxiom } = require("next-axiom");
|
||||
const { version } = require("./package.json");
|
||||
const { i18n } = require("./next-i18next.config");
|
||||
const {
|
||||
orgHostPath,
|
||||
@@ -14,6 +15,9 @@ const {
|
||||
if (!process.env.NEXTAUTH_SECRET) throw new Error("Please set NEXTAUTH_SECRET");
|
||||
if (!process.env.CALENDSO_ENCRYPTION_KEY) throw new Error("Please set CALENDSO_ENCRYPTION_KEY");
|
||||
|
||||
// To be able to use the version in the app without having to import package.json
|
||||
process.env.NEXT_PUBLIC_CALCOM_VERSION = version;
|
||||
|
||||
// So we can test deploy previews preview
|
||||
if (process.env.VERCEL_URL && !process.env.NEXT_PUBLIC_WEBAPP_URL) {
|
||||
process.env.NEXT_PUBLIC_WEBAPP_URL = "https://" + process.env.VERCEL_URL;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.2.0",
|
||||
"version": "3.2.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -68,6 +68,7 @@
|
||||
"@vercel/og": "^0.5.0",
|
||||
"accept-language-parser": "^1.5.0",
|
||||
"async": "^3.2.4",
|
||||
"bcp-47-match": "^2.0.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"classnames": "^2.3.1",
|
||||
"dotenv-cli": "^6.0.0",
|
||||
@@ -166,6 +167,7 @@
|
||||
"msw": "^0.42.3",
|
||||
"postcss": "^8.4.18",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.6",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
|
||||
@@ -75,6 +75,7 @@ export default function Custom404() {
|
||||
)}`
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const isSuccessPage = pathname?.startsWith("/booking");
|
||||
|
||||
@@ -36,6 +36,7 @@ import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
|
||||
|
||||
const [user] = users; //To be used when we only have a single user, not dynamic group
|
||||
useTheme(profile.theme);
|
||||
const { t } = useLocale();
|
||||
@@ -52,7 +53,6 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||
orgSlug: _orgSlug,
|
||||
...query
|
||||
} = useRouterQuery();
|
||||
const nameOrUsername = user.name || user.username || "";
|
||||
|
||||
/*
|
||||
const telemetry = useTelemetry();
|
||||
@@ -82,6 +82,10 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||
profile: { name: `${profile.name}`, image: null },
|
||||
users: [{ username: `${user.username}`, name: `${user.name}` }],
|
||||
}}
|
||||
nextSeoProps={{
|
||||
noindex: !profile.allowSEOIndexing,
|
||||
nofollow: !profile.allowSEOIndexing,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
|
||||
@@ -93,7 +97,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||
)}>
|
||||
<div className="mb-8 text-center">
|
||||
<Avatar imageSrc={profile.image} size="xl" alt={profile.name} />
|
||||
<h1 className="font-cal text-emphasis mb-1 text-3xl">
|
||||
<h1 className="font-cal text-emphasis mb-1 text-3xl" data-testid="name-title">
|
||||
{profile.name}
|
||||
{user.verified && (
|
||||
<Verified className=" mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black" />
|
||||
@@ -214,6 +218,7 @@ export type UserPageProps = {
|
||||
theme: string | null;
|
||||
brandColor: string;
|
||||
darkBrandColor: string;
|
||||
allowSEOIndexing: boolean;
|
||||
};
|
||||
users: Pick<User, "away" | "name" | "username" | "bio" | "verified">[];
|
||||
themeBasis: string | null;
|
||||
@@ -276,6 +281,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
away: true,
|
||||
verified: true,
|
||||
allowDynamicBooking: true,
|
||||
allowSEOIndexing: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -315,6 +321,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
theme: user.theme,
|
||||
brandColor: user.brandColor,
|
||||
darkBrandColor: user.darkBrandColor,
|
||||
allowSEOIndexing: user.allowSEOIndexing ?? true,
|
||||
};
|
||||
|
||||
const eventTypesWithHidden = await getEventTypesWithHiddenFromDB(user.id);
|
||||
|
||||
@@ -30,6 +30,7 @@ export default function Type({
|
||||
booking,
|
||||
away,
|
||||
isBrandingHidden,
|
||||
isSEOIndexable,
|
||||
rescheduleUid,
|
||||
entity,
|
||||
duration,
|
||||
@@ -41,6 +42,7 @@ export default function Type({
|
||||
eventSlug={slug}
|
||||
rescheduleUid={rescheduleUid ?? undefined}
|
||||
hideBranding={isBrandingHidden}
|
||||
isSEOIndexable={isSEOIndexable ?? true}
|
||||
entity={entity}
|
||||
/>
|
||||
<Booker
|
||||
@@ -128,6 +130,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
away: false,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: false,
|
||||
isSEOIndexable: true,
|
||||
themeBasis: null,
|
||||
bookingUid: bookingUid ? `${bookingUid}` : null,
|
||||
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
|
||||
@@ -154,6 +157,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
select: {
|
||||
away: true,
|
||||
hideBranding: true,
|
||||
allowSEOIndexing: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -199,6 +203,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
entity: eventData.entity,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: user?.hideBranding,
|
||||
isSEOIndexable: user?.allowSEOIndexing,
|
||||
themeBasis: username,
|
||||
bookingUid: bookingUid ? `${bookingUid}` : null,
|
||||
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
|
||||
|
||||
@@ -51,7 +51,7 @@ class MyDocument extends Document<Props> {
|
||||
<meta name="msapplication-TileColor" content="#ff0000" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f9fafb" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1C1C" />
|
||||
{(!IS_PRODUCTION || process.env.VERCEL_ENV === "preview") && (
|
||||
{!IS_PRODUCTION && process.env.VERCEL_ENV === "preview" && (
|
||||
// eslint-disable-next-line @next/next/no-sync-scripts
|
||||
<script
|
||||
data-project-id="KjpMrKTnXquJVKfeqmjdTffVPf1a6Unw2LZ58iE4"
|
||||
|
||||
@@ -8,7 +8,7 @@ import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
|
||||
import { IS_CALCOM } from "@calcom/lib/constants";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import { validateUsernameInOrg } from "@calcom/lib/validateUsernameInOrg";
|
||||
import { validateUsernameInTeam, validateUsername } from "@calcom/lib/validateUsername";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
@@ -65,41 +65,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(401).json({ message: "Token expired" });
|
||||
}
|
||||
if (foundToken?.teamId) {
|
||||
const isValidUsername = await validateUsernameInOrg(username, foundToken?.teamId);
|
||||
|
||||
if (!isValidUsername) {
|
||||
return res.status(409).json({ message: "Username already taken" });
|
||||
const teamUserValidation = await validateUsernameInTeam(username, userEmail, foundToken?.teamId);
|
||||
if (!teamUserValidation.isValid) {
|
||||
return res.status(409).json({ message: "Username or email is already taken" });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// There is an existingUser if the username matches
|
||||
// OR if the email matches AND either the email is verified
|
||||
// or both username and password are set
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ username },
|
||||
{
|
||||
AND: [
|
||||
{ email: userEmail },
|
||||
{
|
||||
OR: [
|
||||
{ emailVerified: { not: null } },
|
||||
{
|
||||
AND: [{ password: { not: null } }, { username: { not: null } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
if (existingUser) {
|
||||
const message: string =
|
||||
existingUser.email !== userEmail ? "Username already taken" : "Email address is already registered";
|
||||
|
||||
return res.status(409).json({ message });
|
||||
const userValidation = await validateUsername(username, userEmail);
|
||||
if (!userValidation.isValid) {
|
||||
return res.status(409).json({ message: "Username or email is already taken" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +85,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
id: foundToken.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
if (team) {
|
||||
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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 { totpAuthenticatorCheck } from "@calcom/lib/totp";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { IdentityProvider } from "@calcom/prisma/client";
|
||||
|
||||
@@ -69,7 +69,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
// If user has 2fa enabled, check if body.code is correct
|
||||
const isValidToken = authenticator.check(req.body.code, secret);
|
||||
const isValidToken = totpAuthenticatorCheck(req.body.code, secret);
|
||||
if (!isValidToken) {
|
||||
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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 { totpAuthenticatorCheck } from "@calcom/lib/totp";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -48,7 +48,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||
}
|
||||
|
||||
const isValidToken = authenticator.check(req.body.code, secret);
|
||||
const isValidToken = totpAuthenticatorCheck(req.body.code, secret);
|
||||
if (!isValidToken) {
|
||||
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
|
||||
}
|
||||
|
||||
@@ -7,54 +7,6 @@ import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (IS_PRODUCTION) return res.write("Only for development purposes"), res.end();
|
||||
const t = await getTranslation("en", "common");
|
||||
const language = { translate: t, locale: "en" };
|
||||
|
||||
const evt = {
|
||||
type: "30min",
|
||||
title: "30min between Pro Example and pro@example.com",
|
||||
description: null,
|
||||
additionalNotes: "asdasdas",
|
||||
startTime: "2022-06-03T09:00:00-06:00",
|
||||
endTime: "2022-06-03T09:30:00-06:00",
|
||||
organizer: {
|
||||
name: "Pro Example",
|
||||
email: "pro@example.com",
|
||||
timeZone: "Europe/London",
|
||||
language,
|
||||
},
|
||||
attendees: [
|
||||
{
|
||||
email: "pro@example.com",
|
||||
name: "pro@example.com",
|
||||
timeZone: "America/Chihuahua",
|
||||
language,
|
||||
},
|
||||
],
|
||||
location: "Zoom video",
|
||||
destinationCalendar: null,
|
||||
hideCalendarNotes: false,
|
||||
uid: "xxyPr4cg2xx4XoS2KeMEQy",
|
||||
metadata: {},
|
||||
recurringEvent: null,
|
||||
appsStatus: [
|
||||
{
|
||||
appName: "Outlook Calendar",
|
||||
type: "office365_calendar",
|
||||
success: 1,
|
||||
failures: 0,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
},
|
||||
{
|
||||
appName: "Google Meet",
|
||||
type: "conferencing",
|
||||
success: 0,
|
||||
failures: 1,
|
||||
errors: [],
|
||||
warnings: ["In order to use Google Meet you must set your destination calendar to a Google Calendar"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
res.statusCode = 200;
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import appBasecamp3 from "@calcom/app-store/basecamp3/trpc-router";
|
||||
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
|
||||
|
||||
export default createNextApiHandler(appBasecamp3);
|
||||
@@ -40,9 +40,11 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
|
||||
}>
|
||||
<div className="mb-16">
|
||||
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
{apps.map((app) => {
|
||||
return <AppCard key={app.slug} app={app} />;
|
||||
})}
|
||||
{apps
|
||||
.sort((a, b) => (b.installCount || 0) - (a.installCount || 0))
|
||||
.map((app) => {
|
||||
return <AppCard key={app.slug} app={app} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Shell>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { debounce } from "lodash";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getCsrfToken } from "next-auth/react";
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayout";
|
||||
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { EmptyScreen, showToast } from "@calcom/ui";
|
||||
import { EmptyScreen, showToast, ToggleGroup } from "@calcom/ui";
|
||||
import { Clock } from "@calcom/ui/components/icon";
|
||||
|
||||
import { withQuery } from "@lib/QueryCell";
|
||||
@@ -129,14 +131,52 @@ const WithQuery = withQuery(trpc.viewer.availability.list as any);
|
||||
|
||||
export default function AvailabilityPage() {
|
||||
const { t } = useLocale();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
// Get a new searchParams string by merging the current
|
||||
// searchParams with a provided key/value pair
|
||||
const createQueryString = useCallback(
|
||||
(name: string, value: string) => {
|
||||
const params = new URLSearchParams(searchParams ?? undefined);
|
||||
params.set(name, value);
|
||||
|
||||
return params.toString();
|
||||
},
|
||||
[searchParams]
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<ShellMain
|
||||
heading={t("availability")}
|
||||
hideHeadingOnMobile
|
||||
subtitle={t("configure_availability")}
|
||||
CTA={<NewScheduleButton />}>
|
||||
<WithQuery success={({ data }) => <AvailabilityList {...data} />} customLoader={<SkeletonLoader />} />
|
||||
CTA={
|
||||
<div className="flex gap-2">
|
||||
<ToggleGroup
|
||||
className="hidden md:block"
|
||||
defaultValue={searchParams?.get("type") ?? "mine"}
|
||||
onValueChange={(value) => {
|
||||
if (!value) return;
|
||||
router.push(`${pathname}?${createQueryString("type", value)}`);
|
||||
}}
|
||||
options={[
|
||||
{ value: "mine", label: t("my_availability") },
|
||||
{ value: "team", label: t("team_availability") },
|
||||
]}
|
||||
/>
|
||||
<NewScheduleButton />
|
||||
</div>
|
||||
}>
|
||||
{searchParams?.get("type") === "team" ? (
|
||||
<AvailabilitySliderTable />
|
||||
) : (
|
||||
<WithQuery
|
||||
success={({ data }) => <AvailabilityList {...data} />}
|
||||
customLoader={<SkeletonLoader />}
|
||||
/>
|
||||
)}
|
||||
</ShellMain>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,12 +23,9 @@ import {
|
||||
useIsEmbed,
|
||||
} from "@calcom/embed-core/embed-iframe";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { SystemField } from "@calcom/features/bookings/lib/SystemField";
|
||||
import { SMS_REMINDER_NUMBER_FIELD, SystemField } from "@calcom/features/bookings/lib/SystemField";
|
||||
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
|
||||
import {
|
||||
getBookingFieldsWithSystemFields,
|
||||
SMS_REMINDER_NUMBER_FIELD,
|
||||
} from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import {
|
||||
@@ -96,7 +93,7 @@ const querySchema = z.object({
|
||||
});
|
||||
|
||||
export default function Success(props: SuccessProps) {
|
||||
const { t } = useLocale();
|
||||
const { t, i18n } = useLocale();
|
||||
const router = useRouter();
|
||||
const routerQuery = useRouterQuery();
|
||||
const pathname = usePathname();
|
||||
@@ -113,7 +110,8 @@ export default function Success(props: SuccessProps) {
|
||||
const attendeeTimeZone = props?.bookingInfo?.attendees.find(
|
||||
(attendee) => attendee.email === email
|
||||
)?.timeZone;
|
||||
const tz = isSuccessBookingPage && attendeeTimeZone ? attendeeTimeZone : props.tz ? props.tz : timeZone();
|
||||
|
||||
const tz = props.tz ? props.tz : isSuccessBookingPage && attendeeTimeZone ? attendeeTimeZone : timeZone();
|
||||
|
||||
const location = props.bookingInfo.location as ReturnType<typeof getEventLocationValue>;
|
||||
|
||||
@@ -480,6 +478,21 @@ export default function Success(props: SuccessProps) {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{props.paymentStatus && (
|
||||
<>
|
||||
<div className="mt-3 font-medium">
|
||||
{props.paymentStatus.paymentOption === "HOLD"
|
||||
? t("complete_your_booking")
|
||||
: t("payment")}
|
||||
</div>
|
||||
<div className="col-span-2 mb-2 mt-3">
|
||||
{new Intl.NumberFormat(i18n.language, {
|
||||
style: "currency",
|
||||
currency: props.paymentStatus.currency,
|
||||
}).format(props.paymentStatus.amount / 100.0)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{bookingInfo?.description && (
|
||||
<>
|
||||
<div className="mt-9 font-medium">{t("additional_notes")}</div>
|
||||
@@ -715,7 +728,7 @@ export default function Success(props: SuccessProps) {
|
||||
</div>
|
||||
{isGmail && (
|
||||
<Alert
|
||||
className="main -mb-20 mt-4 inline-block ltr:text-right rtl:text-right sm:-mt-4 sm:mb-4 sm:w-full sm:max-w-xl sm:align-middle"
|
||||
className="main -mb-20 mt-4 inline-block ltr:text-left rtl:text-right sm:-mt-4 sm:mb-4 sm:w-full sm:max-w-xl sm:align-middle"
|
||||
severity="warning"
|
||||
message={
|
||||
<div>
|
||||
@@ -1117,6 +1130,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
select: {
|
||||
success: true,
|
||||
refunded: true,
|
||||
currency: true,
|
||||
amount: true,
|
||||
paymentOption: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ export default function Bookings() {
|
||||
)}
|
||||
<div className="pt-2 xl:pt-0">
|
||||
<div className="border-subtle overflow-hidden rounded-md border">
|
||||
<table className="w-full max-w-full table-fixed">
|
||||
<table data-testid={`${status}-bookings`} className="w-full max-w-full table-fixed">
|
||||
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
|
||||
{query.data.pages.map((page, index) => (
|
||||
<Fragment key={index}>
|
||||
|
||||
@@ -132,6 +132,7 @@ export type FormValues = {
|
||||
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
||||
availability?: AvailabilityOption;
|
||||
bookerLayouts: BookerLayoutSettings;
|
||||
multipleDurationEnabled: boolean;
|
||||
};
|
||||
|
||||
export type CustomInputParsed = typeof customInputSchema._output;
|
||||
@@ -278,6 +279,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
.filter((slug) => slug !== eventType.slug) ?? [],
|
||||
},
|
||||
})),
|
||||
seatsPerTimeSlotEnabled: eventType.seatsPerTimeSlot,
|
||||
};
|
||||
}, [eventType, periodDates, metadata]);
|
||||
|
||||
@@ -375,7 +377,10 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
seatsPerTimeSlotEnabled,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
minimumBookingNoticeInDurationType,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
bookerLayouts,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
multipleDurationEnabled,
|
||||
...input
|
||||
} = values;
|
||||
|
||||
@@ -406,6 +411,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
throw new Error(t("seats_and_no_show_fee_error"));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { availability, ...rest } = input;
|
||||
updateMutation.mutate({
|
||||
...rest,
|
||||
@@ -463,6 +469,8 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
// We don't need to send send these values to the backend
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
seatsPerTimeSlotEnabled,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
multipleDurationEnabled,
|
||||
...input
|
||||
} = values;
|
||||
|
||||
@@ -488,6 +496,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { availability, ...rest } = input;
|
||||
updateMutation.mutate({
|
||||
...rest,
|
||||
|
||||
@@ -737,7 +737,9 @@ const EventTypeListHeading = ({
|
||||
</span>
|
||||
)}
|
||||
{profile?.slug && (
|
||||
<Link href={`${CAL_URL}/${profile.slug}`} className="text-subtle block text-xs">
|
||||
<Link
|
||||
href={`${orgBranding ? orgBranding.fullDomain : CAL_URL}/${profile.slug}`}
|
||||
className="text-subtle block text-xs">
|
||||
{`${bookerUrl.replace("https://", "").replace("http://", "")}/${profile.slug}`}
|
||||
</Link>
|
||||
)}
|
||||
@@ -830,13 +832,13 @@ const SetupProfileBanner = ({ closeAction }: { closeAction: () => void }) => {
|
||||
|
||||
const Main = ({
|
||||
status,
|
||||
error,
|
||||
errorMessage,
|
||||
data,
|
||||
filters,
|
||||
}: {
|
||||
status: string;
|
||||
data: GetByViewerResponse;
|
||||
error: any;
|
||||
errorMessage?: string;
|
||||
filters: ReturnType<typeof getTeamsFiltersFromQuery>;
|
||||
}) => {
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
@@ -848,7 +850,7 @@ const Main = ({
|
||||
}
|
||||
|
||||
if (status === "error") {
|
||||
return <Alert severity="error" title="Something went wrong" message={error.message} />;
|
||||
return <Alert severity="error" title="Something went wrong" message={errorMessage} />;
|
||||
}
|
||||
|
||||
const isFilteredByOnlyOneItem =
|
||||
@@ -945,7 +947,7 @@ const EventTypesPage = () => {
|
||||
title="Event Types"
|
||||
description="Create events to share for people to book on your calendar."
|
||||
/>
|
||||
<Main data={data} status={status} error={error} filters={filters} />
|
||||
<Main data={data} status={status} errorMessage={error?.message} filters={filters} />
|
||||
</ShellMain>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import UnverifiedOrgsPage from "@calcom/features/ee/organizations/pages/settings/admin/unverifiedOrgPage";
|
||||
import AdminOrgsPage from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
|
||||
|
||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
const Page = UnverifiedOrgsPage as CalPageWrapper;
|
||||
const Page = AdminOrgsPage as CalPageWrapper;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -57,7 +57,7 @@ const ApiKeysView = () => {
|
||||
<div>
|
||||
{isLoading ? null : data?.length ? (
|
||||
<>
|
||||
<div className="mb-8 mt-6 rounded-md border">
|
||||
<div className="border-subtle mb-8 mt-6 rounded-md border">
|
||||
{data.map((apiKey, index) => (
|
||||
<ApiKeyListItem
|
||||
key={apiKey.id}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
@@ -20,8 +21,6 @@ import {
|
||||
TimezoneSelect,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { withQuery } from "@lib/QueryCell";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||
@@ -45,11 +44,6 @@ interface GeneralViewProps {
|
||||
user: RouterOutputs["viewer"]["me"];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const WithQuery = withQuery(trpc.viewer.public.i18n as any, undefined, {
|
||||
trpc: { context: { skipBatch: true } },
|
||||
});
|
||||
|
||||
const GeneralQueryView = () => {
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -58,30 +52,26 @@ const GeneralQueryView = () => {
|
||||
if (!user) {
|
||||
throw new Error(t("something_went_wrong"));
|
||||
}
|
||||
return (
|
||||
<WithQuery
|
||||
success={({ data }) => <GeneralView user={user} localeProp={data.locale} />}
|
||||
customLoader={<SkeletonLoader title={t("general")} description={t("general_description")} />}
|
||||
/>
|
||||
);
|
||||
return <GeneralView user={user} localeProp={user.locale} />;
|
||||
};
|
||||
|
||||
const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
||||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
const { update } = useSession();
|
||||
|
||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async () => {
|
||||
// Invalidate our previous i18n cache
|
||||
await utils.viewer.public.i18n.invalidate();
|
||||
onSuccess: async (res) => {
|
||||
await utils.viewer.me.invalidate();
|
||||
reset(getValues());
|
||||
showToast(t("settings_updated_successfully"), "success");
|
||||
update(res);
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("error_updating_settings"), "error");
|
||||
},
|
||||
onSettled: async () => {
|
||||
await utils.viewer.public.i18n.invalidate();
|
||||
await utils.viewer.me.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -116,6 +106,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
||||
label: nameOfDay(localeProp, user.weekStart === "Sunday" ? 0 : 1),
|
||||
},
|
||||
allowDynamicBooking: user.allowDynamicBooking ?? true,
|
||||
allowSEOIndexing: user.allowSEOIndexing ?? true,
|
||||
},
|
||||
});
|
||||
const {
|
||||
@@ -124,6 +115,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
||||
getValues,
|
||||
} = formMethods;
|
||||
const isDisabled = isSubmitting || !isDirty;
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={formMethods}
|
||||
@@ -225,6 +217,24 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<Controller
|
||||
name="allowSEOIndexing"
|
||||
control={formMethods.control}
|
||||
render={() => (
|
||||
<SettingsToggle
|
||||
title={t("seo_indexing")}
|
||||
description={t("allow_seo_indexing")}
|
||||
checked={formMethods.getValues("allowSEOIndexing")}
|
||||
onCheckedChange={(checked) => {
|
||||
formMethods.setValue("allowSEOIndexing", checked, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
loading={mutation.isLoading}
|
||||
disabled={isDisabled}
|
||||
|
||||
@@ -77,26 +77,25 @@ type FormValues = {
|
||||
const ProfileView = () => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const { data: _session, update } = useSession();
|
||||
const { update } = useSession();
|
||||
|
||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
||||
const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async (res) => {
|
||||
await update(res);
|
||||
showToast(t("settings_updated_successfully"), "success");
|
||||
if (res.signOutUser && tempFormValues) {
|
||||
if (res.passwordReset) {
|
||||
showToast(t("password_reset_email", { email: tempFormValues.email }), "success");
|
||||
// sign out the user to avoid unauthorized access error
|
||||
await signOut({ callbackUrl: "/auth/logout?passReset=true" });
|
||||
} else {
|
||||
// sign out the user to avoid unauthorized access error
|
||||
await signOut({ callbackUrl: "/auth/logout?emailChange=true" });
|
||||
}
|
||||
|
||||
// signout user only in case of password reset
|
||||
if (res.signOutUser && tempFormValues && res.passwordReset) {
|
||||
showToast(t("password_reset_email", { email: tempFormValues.email }), "success");
|
||||
await signOut({ callbackUrl: "/auth/logout?passReset=true" });
|
||||
} else {
|
||||
utils.viewer.me.invalidate();
|
||||
utils.viewer.avatar.invalidate();
|
||||
utils.viewer.shouldVerifyEmail.invalidate();
|
||||
}
|
||||
utils.viewer.me.invalidate();
|
||||
utils.viewer.avatar.invalidate();
|
||||
|
||||
setConfirmAuthEmailChangeWarningDialogOpen(false);
|
||||
update(res);
|
||||
setTempFormValues(null);
|
||||
},
|
||||
onError: () => {
|
||||
@@ -325,7 +324,10 @@ const ProfileView = () => {
|
||||
{confirmPasswordErrorMessage && <Alert severity="error" title={confirmPasswordErrorMessage} />}
|
||||
</div>
|
||||
<DialogFooter showDivider>
|
||||
<Button color="primary" onClick={(e) => onConfirmPassword(e)}>
|
||||
<Button
|
||||
color="primary"
|
||||
loading={confirmPasswordMutation.isLoading}
|
||||
onClick={(e) => onConfirmPassword(e)}>
|
||||
{t("confirm")}
|
||||
</Button>
|
||||
<DialogClose />
|
||||
@@ -345,7 +347,7 @@ const ProfileView = () => {
|
||||
<DialogFooter>
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={updateProfileMutation.isLoading}
|
||||
loading={updateProfileMutation.isLoading}
|
||||
onClick={(e) => onConfirmAuthEmailChange(e)}>
|
||||
{t("confirm")}
|
||||
</Button>
|
||||
|
||||
+31
-23
@@ -14,6 +14,7 @@ import { useFlagMap } from "@calcom/features/flags/context/provider";
|
||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
@@ -130,10 +131,11 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
|
||||
<TextField
|
||||
addOnLeading={
|
||||
orgSlug
|
||||
? getOrgFullDomain(orgSlug, { protocol: false })
|
||||
? getOrgFullDomain(orgSlug, { protocol: true })
|
||||
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/`
|
||||
}
|
||||
{...register("username")}
|
||||
disabled={!!orgSlug}
|
||||
required
|
||||
/>
|
||||
<EmailField
|
||||
@@ -192,8 +194,6 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
};
|
||||
|
||||
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" || flags["disable-signup"]) {
|
||||
console.log({ flag: flags["disable-signup"] });
|
||||
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
@@ -210,6 +210,20 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
metadata: true,
|
||||
parentId: true,
|
||||
parent: {
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!verificationToken || verificationToken.expires < new Date()) {
|
||||
@@ -249,41 +263,35 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
|
||||
let username = guessUsernameFromEmail(verificationToken.identifier);
|
||||
|
||||
const orgInfo = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: verificationToken?.identifier,
|
||||
},
|
||||
select: {
|
||||
organization: {
|
||||
select: {
|
||||
slug: true,
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const tokenTeam = {
|
||||
...verificationToken?.team,
|
||||
metadata: teamMetadataSchema.parse(verificationToken?.team?.metadata),
|
||||
};
|
||||
|
||||
const userOrgMetadata = teamMetadataSchema.parse(orgInfo?.organization?.metadata ?? {});
|
||||
// Detect if the team is an org by either the metadata flag or if it has a parent team
|
||||
const isOrganization = tokenTeam.metadata?.isOrganization || tokenTeam?.parentId !== null;
|
||||
// If we are dealing with an org, the slug may come from the team itself or its parent
|
||||
const orgSlug = isOrganization
|
||||
? tokenTeam.slug || tokenTeam.metadata?.requestedSlug || tokenTeam.parent?.slug
|
||||
: null;
|
||||
|
||||
if (!IS_SELF_HOSTED) {
|
||||
// Org context shouldn't check if a username is premium
|
||||
if (!IS_SELF_HOSTED && !isOrganization) {
|
||||
// Im not sure we actually hit this because of next redirects signup to website repo - but just in case this is pretty cool :)
|
||||
const { available, suggestion } = await checkPremiumUsername(username);
|
||||
|
||||
username = available ? username : suggestion || username;
|
||||
}
|
||||
|
||||
// Transform all + to - in username
|
||||
username = username.replace(/\+/g, "-");
|
||||
|
||||
return {
|
||||
props: {
|
||||
...props,
|
||||
token,
|
||||
prepopulateFormValues: {
|
||||
email: verificationToken.identifier,
|
||||
username,
|
||||
username: slugify(username),
|
||||
},
|
||||
orgSlug: (orgInfo?.organization?.slug || userOrgMetadata?.requestedSlug) ?? null,
|
||||
orgSlug,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -182,7 +182,7 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
<p className="font-cal text-emphasis mb-2 text-2xl tracking-wider">
|
||||
<p className="font-cal text-emphasis mb-2 text-2xl tracking-wider" data-testid="team-name">
|
||||
{team.parent && `${team.parent.name} `}
|
||||
{teamName}
|
||||
</p>
|
||||
@@ -199,19 +199,27 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
|
||||
<SubTeams />
|
||||
) : (
|
||||
<>
|
||||
{(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />}
|
||||
{(showMembers.isOn || !team.eventTypes.length) &&
|
||||
(team.isPrivate ? (
|
||||
<div className="w-full text-center">
|
||||
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_team_members")}</h2>
|
||||
</div>
|
||||
) : (
|
||||
<Team team={team} />
|
||||
))}
|
||||
{!showMembers.isOn && team.eventTypes.length > 0 && (
|
||||
<div className="mx-auto max-w-3xl ">
|
||||
<EventTypes />
|
||||
|
||||
{!team.hideBookATeamMember && (
|
||||
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
|
||||
{(!team.hideBookATeamMember || team.isPrivate) && (
|
||||
<div>
|
||||
<div className="relative mt-12">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="border-subtle w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="dark:bg-darkgray-50 bg-subtle text-subtle dark:text-inverted px-2 text-sm">
|
||||
<span className="dark:bg-darkgray-50 bg-subtle text-subtle px-2 text-sm">
|
||||
{t("or")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { installAppleCalendar } from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
@@ -10,10 +11,9 @@ test.describe("App Store - Authed", () => {
|
||||
test("Browse apple-calendar and try to install", async ({ page, users }) => {
|
||||
const pro = await users.create();
|
||||
await pro.apiLogin();
|
||||
await page.goto("/apps/categories/calendar");
|
||||
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
|
||||
await page.waitForURL("/apps/apple-calendar");
|
||||
await page.click('[data-testid="install-app-button"]');
|
||||
|
||||
await installAppleCalendar(page);
|
||||
|
||||
await expect(page.locator(`text=Connect to Apple Server`)).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -31,9 +31,8 @@ test.describe("App Store - Authed", () => {
|
||||
|
||||
test.describe("App Store - Unauthed", () => {
|
||||
test("Browse apple-calendar and try to install", async ({ page }) => {
|
||||
await page.goto("/apps/categories/calendar");
|
||||
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
|
||||
await page.click('[data-testid="install-app-button"]');
|
||||
await installAppleCalendar(page);
|
||||
|
||||
await expect(page.locator(`[data-testid="login-form"]`)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { installAppleCalendar } from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
const APPLE_CALENDAR_EMAIL = process.env.E2E_TEST_APPLE_CALENDAR_EMAIL!;
|
||||
const APPLE_CALENDAR_PASSWORD = process.env.E2E_TEST_APPLE_CALENDAR_PASSWORD!;
|
||||
|
||||
const SHOULD_SKIP_TESTS = !APPLE_CALENDAR_EMAIL || !APPLE_CALENDAR_PASSWORD;
|
||||
|
||||
test.describe("Apple Calendar", () => {
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(SHOULD_SKIP_TESTS, "Skipping due to missing the testing credentials");
|
||||
|
||||
test("Should be able to install and login on Apple Calendar", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.apiLogin();
|
||||
|
||||
await installAppleCalendar(page);
|
||||
|
||||
await expect(page.locator('[data-testid="apple-calendar-form"]')).toBeVisible();
|
||||
|
||||
await page.fill('[data-testid="apple-calendar-email"]', APPLE_CALENDAR_EMAIL);
|
||||
await page.fill('[data-testid="apple-calendar-password"]', APPLE_CALENDAR_PASSWORD);
|
||||
await page.click('[data-testid="apple-calendar-login-button"]');
|
||||
|
||||
await expect(page.getByText("Apple Calendar")).toBeVisible();
|
||||
await expect(page.getByText(APPLE_CALENDAR_EMAIL)).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import type { Messages } from "mailhog";
|
||||
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
bookFirstEvent,
|
||||
bookOptinEvent,
|
||||
bookTimeSlot,
|
||||
expectEmailsToHaveSubject,
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
testEmail,
|
||||
testName,
|
||||
@@ -41,19 +41,13 @@ test.describe("free user", () => {
|
||||
// Make sure we're navigated to the success page
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
const { title: eventTitle } = await user.getFirstEventAsOwner();
|
||||
// TODO: follow DRY
|
||||
const emailsOrganiserReceived = await emails.search(user.email, "to");
|
||||
const emailsBookerReceived = await emails.search(bookerObj.email, "to");
|
||||
expect(emailsOrganiserReceived?.total).toBe(1);
|
||||
expect(emailsBookerReceived?.total).toBe(1);
|
||||
|
||||
const [organizerFirstEmail] = (emailsOrganiserReceived as Messages).items;
|
||||
const [bookerFirstEmail] = (emailsBookerReceived as Messages).items;
|
||||
const emailSubject = `${eventTitle} between ${user.name} and ${bookerObj.name}`;
|
||||
|
||||
expect(organizerFirstEmail.subject).toBe(emailSubject);
|
||||
expect(bookerFirstEmail.subject).toBe(emailSubject);
|
||||
|
||||
await expectEmailsToHaveSubject({
|
||||
emails,
|
||||
organizer: user,
|
||||
booker: bookerObj,
|
||||
eventTitle,
|
||||
});
|
||||
await page.goto(bookingUrl);
|
||||
|
||||
// book same time spot again
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { BookingStatus } from "@calcom/prisma/client";
|
||||
|
||||
import type { Fixtures } from "./lib/fixtures";
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
test.describe("Bookings", () => {
|
||||
test.describe("Upcoming bookings", () => {
|
||||
test("show attendee bookings and organizer bookings in asc order by startDate", async ({
|
||||
page,
|
||||
users,
|
||||
bookings,
|
||||
}) => {
|
||||
const firstUser = await users.create();
|
||||
const secondUser = await users.create();
|
||||
|
||||
const bookingWhereFirstUserIsOrganizerFixture = await createBooking({
|
||||
title: "Booking as organizer",
|
||||
bookingsFixture: bookings,
|
||||
// Create a booking 3 days from today
|
||||
relativeDate: 3,
|
||||
organizer: firstUser,
|
||||
organizerEventType: firstUser.eventTypes[0],
|
||||
attendees: [
|
||||
{ name: "First", email: "first@cal.com", timeZone: "Europe/Berlin" },
|
||||
{ name: "Second", email: "second@cal.com", timeZone: "Europe/Berlin" },
|
||||
{ name: "Third", email: "third@cal.com", timeZone: "Europe/Berlin" },
|
||||
],
|
||||
});
|
||||
const bookingWhereFirstUserIsOrganizer = await bookingWhereFirstUserIsOrganizerFixture.self();
|
||||
|
||||
const bookingWhereFirstUserIsAttendeeFixture = await createBooking({
|
||||
title: "Booking as attendee",
|
||||
bookingsFixture: bookings,
|
||||
organizer: secondUser,
|
||||
// Booking created 2 days from today
|
||||
relativeDate: 2,
|
||||
organizerEventType: secondUser.eventTypes[0],
|
||||
attendees: [
|
||||
{ name: "OrganizerAsBooker", email: firstUser.email, timeZone: "Europe/Berlin" },
|
||||
{ name: "Second", email: "second@cal.com", timeZone: "Europe/Berlin" },
|
||||
{ name: "Third", email: "third@cal.com", timeZone: "Europe/Berlin" },
|
||||
],
|
||||
});
|
||||
const bookingWhereFirstUserIsAttendee = await bookingWhereFirstUserIsAttendeeFixture.self();
|
||||
|
||||
await firstUser.apiLogin();
|
||||
await page.goto(`/bookings/upcoming`);
|
||||
const upcomingBookings = page.locator('[data-testid="upcoming-bookings"]');
|
||||
const firstUpcomingBooking = upcomingBookings.locator('[data-testid="booking-item"]').nth(0);
|
||||
const secondUpcomingBooking = upcomingBookings.locator('[data-testid="booking-item"]').nth(1);
|
||||
await expect(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
firstUpcomingBooking.locator(`text=${bookingWhereFirstUserIsAttendee!.title}`)
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
secondUpcomingBooking.locator(`text=${bookingWhereFirstUserIsOrganizer!.title}`)
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createBooking({
|
||||
bookingsFixture,
|
||||
organizer,
|
||||
organizerEventType,
|
||||
attendees,
|
||||
/**
|
||||
* Relative date from today
|
||||
* 0 means today
|
||||
* 1 means tomorrow
|
||||
*/
|
||||
relativeDate = 0,
|
||||
durationMins = 30,
|
||||
title,
|
||||
}: {
|
||||
bookingsFixture: Fixtures["bookings"];
|
||||
organizer: {
|
||||
id: number;
|
||||
username: string | null;
|
||||
};
|
||||
organizerEventType: {
|
||||
id: number;
|
||||
};
|
||||
attendees: {
|
||||
name: string;
|
||||
email: string;
|
||||
timeZone: string;
|
||||
}[];
|
||||
relativeDate?: number;
|
||||
durationMins?: number;
|
||||
title: string;
|
||||
}) {
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const bookingDurationMs = durationMins * 60 * 1000;
|
||||
const startTime = new Date(Date.now() + relativeDate * DAY_MS);
|
||||
const endTime = new Date(Date.now() + relativeDate * DAY_MS + bookingDurationMs);
|
||||
return await bookingsFixture.create(organizer.id, organizer.username, organizerEventType.id, {
|
||||
title,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime,
|
||||
endTime,
|
||||
attendees: {
|
||||
createMany: {
|
||||
data: [...attendees],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
@@ -91,6 +92,7 @@ test.describe("Event Types tests", () => {
|
||||
expect(formTitle).toBe(firstTitle);
|
||||
expect(formSlug).toContain(firstSlug);
|
||||
});
|
||||
|
||||
test("edit first event", async ({ page }) => {
|
||||
const $eventTypes = page.locator("[data-testid=event-types] > li a");
|
||||
const firstEventTypeElement = $eventTypes.first();
|
||||
@@ -152,5 +154,46 @@ test.describe("Event Types tests", () => {
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("Different Locations Tests", () => {
|
||||
test("can add Attendee Phone Number location and book with it", async ({ page }) => {
|
||||
await gotoFirstEventType(page);
|
||||
await selectAttendeePhoneNumber(page);
|
||||
await saveEventType(page);
|
||||
await gotoBookingPage(page);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
await page.locator(`[data-fob-field-name="location"] input`).fill("9199999999");
|
||||
await bookTimeSlot(page);
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
await expect(page.locator("text=+19199999999")).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const selectAttendeePhoneNumber = async (page: Page) => {
|
||||
const locationOptionText = "Attendee Phone Number";
|
||||
await page.locator("#location-select").click();
|
||||
await page.locator(`text=${locationOptionText}`).click();
|
||||
};
|
||||
|
||||
async function gotoFirstEventType(page: Page) {
|
||||
const $eventTypes = page.locator("[data-testid=event-types] > li a");
|
||||
const firstEventTypeElement = $eventTypes.first();
|
||||
await firstEventTypeElement.click();
|
||||
await page.waitForURL((url) => {
|
||||
return !!url.pathname.match(/\/event-types\/.+/);
|
||||
});
|
||||
}
|
||||
|
||||
async function saveEventType(page: Page) {
|
||||
await page.locator("[data-testid=update-eventtype]").click();
|
||||
}
|
||||
|
||||
async function gotoBookingPage(page: Page) {
|
||||
const previewLink = await page.locator("[data-testid=preview-button]").getAttribute("href");
|
||||
|
||||
await page.goto(previewLink ?? "");
|
||||
}
|
||||
|
||||
@@ -19,9 +19,12 @@ export const createBookingsFixture = (page: Page) => {
|
||||
username: string | null,
|
||||
eventTypeId = -1,
|
||||
{
|
||||
title = "",
|
||||
rescheduled = false,
|
||||
paid = false,
|
||||
status = "ACCEPTED",
|
||||
startTime,
|
||||
endTime,
|
||||
attendees = {
|
||||
create: {
|
||||
email: "attendee@example.com",
|
||||
@@ -39,9 +42,9 @@ export const createBookingsFixture = (page: Page) => {
|
||||
const booking = await prisma.booking.create({
|
||||
data: {
|
||||
uid: uid,
|
||||
title: "30min",
|
||||
startTime: startDate,
|
||||
endTime: endDateParam || dayjs().add(1, "day").add(30, "minutes").toDate(),
|
||||
title: title || "30min",
|
||||
startTime: startTime || startDate,
|
||||
endTime: endTime || endDateParam || dayjs().add(1, "day").add(30, "minutes").toDate(),
|
||||
user: {
|
||||
connect: {
|
||||
id: userId,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { hashSync as hash } from "bcryptjs";
|
||||
import type { API } from "mailhog";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||
@@ -117,7 +118,7 @@ const createTeamAndAddUser = async (
|
||||
};
|
||||
|
||||
// creates a user fixture instance and stores the collection
|
||||
export const createUsersFixture = (page: Page, emails: API, workerInfo: WorkerInfo) => {
|
||||
export const createUsersFixture = (page: Page, emails: API | undefined, workerInfo: WorkerInfo) => {
|
||||
const store = { users: [], page } as { users: UserFixture[]; page: typeof page };
|
||||
return {
|
||||
create: async (
|
||||
@@ -331,19 +332,22 @@ export const createUsersFixture = (page: Page, emails: API, workerInfo: WorkerIn
|
||||
await page.goto("/auth/logout");
|
||||
},
|
||||
deleteAll: async () => {
|
||||
const emailMessageIds: string[] = [];
|
||||
const ids = store.users.map((u) => u.id);
|
||||
for (const user of store.users) {
|
||||
const emailMessages = await emails.search(user.email);
|
||||
if (emailMessages && emailMessages.count > 0) {
|
||||
emailMessages.items.forEach((item) => {
|
||||
emailMessageIds.push(item.ID);
|
||||
});
|
||||
if (emails) {
|
||||
const emailMessageIds: string[] = [];
|
||||
for (const user of store.users) {
|
||||
const emailMessages = await emails.search(user.email);
|
||||
if (emailMessages && emailMessages.count > 0) {
|
||||
emailMessages.items.forEach((item) => {
|
||||
emailMessageIds.push(item.ID);
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const id of emailMessageIds) {
|
||||
await emails.deleteMessage(id);
|
||||
}
|
||||
}
|
||||
for (const id of emailMessageIds) {
|
||||
await emails.deleteMessage(id);
|
||||
}
|
||||
|
||||
await prisma.user.deleteMany({ where: { id: { in: ids } } });
|
||||
store.users = [];
|
||||
},
|
||||
@@ -416,8 +420,8 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
||||
getPaymentCredential: async () => getPaymentCredential(store.page),
|
||||
setupEventWithPrice: async (eventType: Pick<Prisma.EventType, "id">) =>
|
||||
setupEventWithPrice(eventType, store.page),
|
||||
bookAndPaidEvent: async (eventType: Pick<Prisma.EventType, "slug">) =>
|
||||
bookAndPaidEvent(user, eventType, store.page),
|
||||
bookAndPayEvent: async (eventType: Pick<Prisma.EventType, "slug">) =>
|
||||
bookAndPayEvent(user, eventType, store.page),
|
||||
makePaymentUsingStripe: async () => makePaymentUsingStripe(store.page),
|
||||
// ths is for developemnt only aimed to inject debugging messages in the metadata field of the user
|
||||
debug: async (message: string | Record<string, JSONValue>) => {
|
||||
@@ -427,6 +431,7 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
||||
});
|
||||
},
|
||||
delete: async () => await prisma.user.delete({ where: { id: store.user.id } }),
|
||||
confirmPendingPayment: async () => confirmPendingPayment(store.page),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -437,12 +442,18 @@ type CustomUserOptsKeys = "username" | "password" | "completedOnboarding" | "loc
|
||||
type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
|
||||
timeZone?: TimeZoneEnum;
|
||||
eventTypes?: SupportedTestEventTypes[];
|
||||
// ignores adding the worker-index after username
|
||||
useExactUsername?: boolean;
|
||||
};
|
||||
|
||||
// creates the actual user in the db.
|
||||
const createUser = (workerInfo: WorkerInfo, opts?: CustomUserOpts | null): PrismaType.UserCreateInput => {
|
||||
// build a unique name for our user
|
||||
const uname = `${opts?.username || "user"}-${workerInfo.workerIndex}-${Date.now()}`;
|
||||
const uname =
|
||||
opts?.useExactUsername && opts?.username
|
||||
? opts.username
|
||||
: `${opts?.username || "user"}-${workerInfo.workerIndex}-${Date.now()}`;
|
||||
|
||||
return {
|
||||
username: uname,
|
||||
name: opts?.name,
|
||||
@@ -468,6 +479,36 @@ const createUser = (workerInfo: WorkerInfo, opts?: CustomUserOpts | null): Prism
|
||||
};
|
||||
};
|
||||
|
||||
async function confirmPendingPayment(page: Page) {
|
||||
await page.waitForURL(new RegExp("/booking/*"));
|
||||
|
||||
const url = page.url();
|
||||
|
||||
const params = new URLSearchParams(url.split("?")[1]);
|
||||
|
||||
const id = params.get("payment_intent");
|
||||
|
||||
if (!id) throw new Error(`Payment intent not found in url ${url}`);
|
||||
|
||||
const payload = JSON.stringify(
|
||||
{ type: "payment_intent.succeeded", data: { object: { id } }, account: "e2e_test" },
|
||||
null,
|
||||
2
|
||||
);
|
||||
|
||||
const signature = stripe.webhooks.generateTestHeaderString({
|
||||
payload,
|
||||
secret: process.env.STRIPE_WEBHOOK_SECRET as string,
|
||||
});
|
||||
|
||||
const response = await page.request.post("/api/integrations/stripepayment/webhook", {
|
||||
data: payload,
|
||||
headers: { "stripe-signature": signature },
|
||||
});
|
||||
|
||||
if (response.status() !== 200) throw new Error(`Failed to confirm payment. Response: ${response.text()}`);
|
||||
}
|
||||
|
||||
// login using a replay of an E2E routine.
|
||||
export async function login(
|
||||
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "password" | "email">>,
|
||||
@@ -501,7 +542,7 @@ export async function apiLogin(
|
||||
.then((json) => json.csrfToken);
|
||||
const data = {
|
||||
email: user.email ?? `${user.username}@example.com`,
|
||||
password: user.password ?? user.username!,
|
||||
password: user.password ?? user.username,
|
||||
callbackURL: "http://localhost:3000/",
|
||||
redirect: "false",
|
||||
json: "true",
|
||||
@@ -519,7 +560,7 @@ export async function setupEventWithPrice(eventType: Pick<Prisma.EventType, "id"
|
||||
await page.getByTestId("update-eventtype").click();
|
||||
}
|
||||
|
||||
export async function bookAndPaidEvent(
|
||||
export async function bookAndPayEvent(
|
||||
user: Pick<Prisma.User, "username">,
|
||||
eventType: Pick<Prisma.EventType, "slug">,
|
||||
page: Page
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import type Prisma from "@prisma/client";
|
||||
|
||||
import type { Fixtures } from "./lib/fixtures";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { todo, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
|
||||
|
||||
@@ -41,7 +42,7 @@ test.describe("Stripe integration", () => {
|
||||
|
||||
await user.getPaymentCredential();
|
||||
await user.setupEventWithPrice(eventType);
|
||||
await user.bookAndPaidEvent(eventType);
|
||||
await user.bookAndPayEvent(eventType);
|
||||
// success
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
});
|
||||
@@ -78,7 +79,7 @@ test.describe("Stripe integration", () => {
|
||||
|
||||
await user.getPaymentCredential();
|
||||
await user.setupEventWithPrice(eventType);
|
||||
await user.bookAndPaidEvent(eventType);
|
||||
await user.bookAndPayEvent(eventType);
|
||||
|
||||
// Rescheduling the event
|
||||
await Promise.all([page.waitForURL("/booking/*"), page.click('[data-testid="reschedule-link"]')]);
|
||||
@@ -92,8 +93,57 @@ test.describe("Stripe integration", () => {
|
||||
|
||||
await user.makePaymentUsingStripe();
|
||||
});
|
||||
todo("Payment should confirm pending payment booking");
|
||||
todo("Payment should trigger a BOOKING_PAID webhook");
|
||||
todo("Paid booking should be able to be cancelled");
|
||||
todo("Cancelled paid booking should be refunded");
|
||||
|
||||
test("Paid booking should be able to be cancelled", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType;
|
||||
await user.apiLogin();
|
||||
await page.goto("/apps/installed");
|
||||
|
||||
await user.getPaymentCredential();
|
||||
await user.setupEventWithPrice(eventType);
|
||||
await user.bookAndPayEvent(eventType);
|
||||
|
||||
await page.click('[data-testid="cancel"]');
|
||||
await page.click('[data-testid="confirm_cancel"]');
|
||||
|
||||
await expect(await page.locator('[data-testid="cancelled-headline"]').first()).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("When event is paid and confirmed", () => {
|
||||
let user: Awaited<ReturnType<Fixtures["users"]["create"]>>;
|
||||
let eventType: Prisma.EventType;
|
||||
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
user = await users.create();
|
||||
eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType;
|
||||
await user.apiLogin();
|
||||
await page.goto("/apps/installed");
|
||||
|
||||
await user.getPaymentCredential();
|
||||
await user.setupEventWithPrice(eventType);
|
||||
await user.bookAndPayEvent(eventType);
|
||||
await user.confirmPendingPayment();
|
||||
});
|
||||
|
||||
test("Cancelled paid booking should be refunded", async ({ page, users, request }) => {
|
||||
await page.click('[data-testid="cancel"]');
|
||||
await page.click('[data-testid="confirm_cancel"]');
|
||||
|
||||
await expect(await page.locator('[data-testid="cancelled-headline"]').first()).toBeVisible();
|
||||
await expect(page.getByText("This booking payment has been refunded")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Payment should confirm pending payment booking", async ({ page, users }) => {
|
||||
await page.goto("/bookings/upcoming");
|
||||
|
||||
const paidBadge = page.locator('[data-testid="paid_badge"]').first();
|
||||
|
||||
await expect(paidBadge).toBeVisible();
|
||||
expect(await paidBadge.innerText()).toBe("Paid");
|
||||
});
|
||||
|
||||
todo("Payment should trigger a BOOKING_PAID webhook");
|
||||
todo("Paid and confirmed booking should be able to be rescheduled");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { test as base } from "@playwright/test";
|
||||
import type { API } from "mailhog";
|
||||
import mailhog from "mailhog";
|
||||
|
||||
import { IS_MAILHOG_ENABLED } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { ExpectedUrlDetails } from "../../../../playwright.config";
|
||||
@@ -21,7 +22,7 @@ export interface Fixtures {
|
||||
getActionFiredDetails: ReturnType<typeof createGetActionFiredDetails>;
|
||||
servers: ReturnType<typeof createServersFixture>;
|
||||
prisma: typeof prisma;
|
||||
emails: API;
|
||||
emails?: API;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -71,7 +72,11 @@ export const test = base.extend<Fixtures>({
|
||||
await use(prisma);
|
||||
},
|
||||
emails: async ({}, use) => {
|
||||
const mailhogAPI = mailhog();
|
||||
await use(mailhogAPI);
|
||||
if (IS_MAILHOG_ENABLED) {
|
||||
const mailhogAPI = mailhog();
|
||||
await use(mailhogAPI);
|
||||
} else {
|
||||
await use(undefined);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,7 +2,9 @@ import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import type { IncomingMessage, ServerResponse } from "http";
|
||||
import { createServer } from "http";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { noop } from "lodash";
|
||||
import type { API, Messages } from "mailhog";
|
||||
|
||||
import { test } from "./fixtures";
|
||||
|
||||
@@ -183,3 +185,46 @@ export async function gotoRoutingLink({
|
||||
// HACK: There seems to be some issue with the inputs to the form getting reset if we don't wait.
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
export async function installAppleCalendar(page: Page) {
|
||||
await page.goto("/apps/categories/calendar");
|
||||
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
|
||||
await page.waitForURL("/apps/apple-calendar");
|
||||
await page.click('[data-testid="install-app-button"]');
|
||||
}
|
||||
export async function getEmailsReceivedByUser({
|
||||
emails,
|
||||
userEmail,
|
||||
}: {
|
||||
emails?: API;
|
||||
userEmail: string;
|
||||
}): Promise<Messages | null> {
|
||||
if (!emails) return null;
|
||||
return emails.search(userEmail, "to");
|
||||
}
|
||||
|
||||
export async function expectEmailsToHaveSubject({
|
||||
emails,
|
||||
organizer,
|
||||
booker,
|
||||
eventTitle,
|
||||
}: {
|
||||
emails?: API;
|
||||
organizer: { name?: string | null; email: string };
|
||||
booker: { name: string; email: string };
|
||||
eventTitle: string;
|
||||
}) {
|
||||
if (!emails) return null;
|
||||
const emailsOrganizerReceived = await getEmailsReceivedByUser({ emails, userEmail: organizer.email });
|
||||
const emailsBookerReceived = await getEmailsReceivedByUser({ emails, userEmail: booker.email });
|
||||
|
||||
expect(emailsOrganizerReceived?.total).toBe(1);
|
||||
expect(emailsBookerReceived?.total).toBe(1);
|
||||
|
||||
const [organizerFirstEmail] = (emailsOrganizerReceived as Messages).items;
|
||||
const [bookerFirstEmail] = (emailsBookerReceived as Messages).items;
|
||||
const emailSubject = `${eventTitle} between ${organizer.name ?? "Nameless"} and ${booker.name}`;
|
||||
|
||||
expect(organizerFirstEmail.subject).toBe(emailSubject);
|
||||
expect(bookerFirstEmail.subject).toBe(emailSubject);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { expect } from "@playwright/test";
|
||||
import { authenticator } from "otplib";
|
||||
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
@@ -141,7 +142,7 @@ test.describe("2FA Tests", async () => {
|
||||
|
||||
async function fillOtp({ page, secret, noRetry }: { page: Page; secret: string; noRetry?: boolean }) {
|
||||
let token = authenticator.generate(secret);
|
||||
if (!noRetry && !authenticator.check(token, secret)) {
|
||||
if (!noRetry && !totpAuthenticatorCheck(token, secret)) {
|
||||
console.log("Token expired, Renerating.");
|
||||
// Maybe token was just about to expire, try again just once more
|
||||
token = authenticator.generate(secret);
|
||||
|
||||
@@ -202,7 +202,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
|
||||
await addQuestionAndSave({
|
||||
page,
|
||||
question: {
|
||||
name: "how_are_you",
|
||||
name: "how-are-you",
|
||||
type: "Address",
|
||||
label: "How are you?",
|
||||
placeholder: "I'm fine, thanks",
|
||||
@@ -214,7 +214,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
|
||||
const allFieldsLocator = await expectSystemFieldsToBeThereOnBookingPage({ page });
|
||||
const userFieldLocator = allFieldsLocator.nth(5);
|
||||
|
||||
await expect(userFieldLocator.locator('[name="how_are_you"]')).toBeVisible();
|
||||
await expect(userFieldLocator.locator('[name="how-are-you"]')).toBeVisible();
|
||||
// There are 2 labels right now. Will be one in future. The second one is hidden
|
||||
expect(await getLabelText(userFieldLocator)).toBe("How are you?");
|
||||
await expect(userFieldLocator.locator("input")).toBeVisible();
|
||||
@@ -223,18 +223,18 @@ async function runTestStepsCommonForTeamAndUserEventType(
|
||||
|
||||
await test.step("Hide Question and see that it's not shown on Booking Page", async () => {
|
||||
await toggleQuestionAndSave({
|
||||
name: "how_are_you",
|
||||
name: "how-are-you",
|
||||
page,
|
||||
});
|
||||
await doOnFreshPreview(page, context, async (page) => {
|
||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how-are-you"]');
|
||||
await expect(formBuilderFieldLocator).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Show Question Again", async () => {
|
||||
await toggleQuestionAndSave({
|
||||
name: "how_are_you",
|
||||
name: "how-are-you",
|
||||
page,
|
||||
});
|
||||
});
|
||||
@@ -242,7 +242,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
|
||||
await test.step('Try to book without providing "How are you?" response', async () => {
|
||||
await doOnFreshPreview(page, context, async (page) => {
|
||||
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
|
||||
await expectErrorToBeThereFor({ page, name: "how_are_you" });
|
||||
await expectErrorToBeThereFor({ page, name: "how-are-you" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -260,18 +260,18 @@ async function runTestStepsCommonForTeamAndUserEventType(
|
||||
page,
|
||||
context,
|
||||
async (page) => {
|
||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how-are-you"]');
|
||||
await expect(formBuilderFieldLocator).toBeVisible();
|
||||
expect(
|
||||
await formBuilderFieldLocator.locator('[name="how_are_you"]').getAttribute("placeholder")
|
||||
await formBuilderFieldLocator.locator('[name="how-are-you"]').getAttribute("placeholder")
|
||||
).toBe("I'm fine, thanks");
|
||||
expect(await getLabelText(formBuilderFieldLocator)).toBe("How are you?");
|
||||
await formBuilderFieldLocator.locator('[name="how_are_you"]').fill("I am great!");
|
||||
await formBuilderFieldLocator.locator('[name="how-are-you"]').fill("I am great!");
|
||||
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
expect(
|
||||
await page.locator('[data-testid="field-response"][data-fob-field="how_are_you"]').innerText()
|
||||
await page.locator('[data-testid="field-response"][data-fob-field="how-are-you"]').innerText()
|
||||
).toBe("I am great!");
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -287,7 +287,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
|
||||
label: "email_address",
|
||||
value: "booker@example.com",
|
||||
},
|
||||
how_are_you: {
|
||||
"how-are-you": {
|
||||
label: "How are you?",
|
||||
value: "I am great!",
|
||||
},
|
||||
@@ -303,7 +303,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
|
||||
});
|
||||
|
||||
expect(payload.userFieldsResponses).toMatchObject({
|
||||
how_are_you: {
|
||||
"how-are-you": {
|
||||
label: "How are you?",
|
||||
value: "I am great!",
|
||||
},
|
||||
|
||||
@@ -135,6 +135,48 @@ test.describe("Teams", () => {
|
||||
expect(teamMatesObj.some(({ name }) => name === chosenUser)).toBe(true);
|
||||
// TODO: Assert whether the user received an email
|
||||
});
|
||||
test("Can create team with same name as user", async ({ page, users }) => {
|
||||
// Name to be used for both user and team
|
||||
const uniqueName = "test-unique-name";
|
||||
const ownerObj = { username: uniqueName, name: uniqueName, useExactUsername: true };
|
||||
|
||||
const user = await users.create(ownerObj);
|
||||
await user.apiLogin();
|
||||
await page.goto("/teams");
|
||||
|
||||
await test.step("Can create team with same name", async () => {
|
||||
// Click text=Create Team
|
||||
await page.locator("text=Create Team").click();
|
||||
await page.waitForURL("/settings/teams/new");
|
||||
// Fill input[name="name"]
|
||||
await page.locator('input[name="name"]').fill(uniqueName);
|
||||
// Click text=Continue
|
||||
await page.locator("text=Continue").click();
|
||||
await page.waitForURL(/\/settings\/teams\/(\d+)\/onboard-members$/i);
|
||||
// Click text=Continue
|
||||
await page.locator("text=Publish team").click();
|
||||
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||
});
|
||||
|
||||
await test.step("Can access user and team with same slug", async () => {
|
||||
// Go to team page and confirm name
|
||||
const teamUrl = `/team/${uniqueName}`;
|
||||
await page.goto(teamUrl);
|
||||
await page.waitForURL(teamUrl);
|
||||
await expect(page.locator("[data-testid=team-name]")).toHaveText(uniqueName);
|
||||
|
||||
// Go to user page and confirm name
|
||||
const userUrl = `/${uniqueName}`;
|
||||
await page.goto(userUrl);
|
||||
await page.waitForURL(userUrl);
|
||||
await expect(page.locator("[data-testid=name-title]")).toHaveText(uniqueName);
|
||||
|
||||
// cleanup team
|
||||
const team = await prisma.team.findFirst({ where: { slug: uniqueName } });
|
||||
await prisma.team.delete({ where: { id: team?.id } });
|
||||
});
|
||||
});
|
||||
|
||||
todo("Create a Round Robin with different leastRecentlyBooked hosts");
|
||||
todo("Reschedule a Collective EventType booking");
|
||||
todo("Reschedule a Round Robin EventType booking");
|
||||
|
||||
@@ -184,6 +184,7 @@ test.describe("BOOKING_REJECTED", async () => {
|
||||
expect(webhookReceiver.requestList.length).toBe(1);
|
||||
});
|
||||
const [request] = webhookReceiver.requestList;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const body = request.body as any;
|
||||
|
||||
// remove dynamic properties that differs depending on where you run the tests
|
||||
|
||||
@@ -13,15 +13,15 @@
|
||||
"reset_password_subject": "{{appName}}: إرشادات إعادة تعيين كلمة المرور",
|
||||
"verify_email_subject": "{{appName}}: تأكيد حسابك",
|
||||
"check_your_email": "تحقق من بريدك الإلكتروني",
|
||||
"verify_email_page_body": "أرسلنا رسالة إلكترونية إلى {{email}}. من المهم تأكيد عنوان بريدك الإلكتروني لضمان أفضل قابلية تسليم للبريد الإلكتروني والرزنامة من {{appName}}.",
|
||||
"verify_email_banner_body": "قم بتأكيد عنوان بريدك الإلكتروني لضمان أفضل تسليم للبريد الإلكتروني والرزنامة",
|
||||
"verify_email_page_body": "أرسلنا رسالة إلكترونية إلى {{email}}. من المهم تأكيد عنوان بريدك الإلكتروني لضمان أفضل تسليم للبريد الإلكتروني والتقويم من {{appName}}.",
|
||||
"verify_email_banner_body": "قم بتأكيد عنوان بريدك الإلكتروني لضمان أفضل تسليم للبريد الإلكتروني والتقويم",
|
||||
"verify_email_email_header": "تأكيد عنوان بريدك الإلكتروني",
|
||||
"verify_email_email_button": "تأكيد البريد الإلكتروني",
|
||||
"verify_email_email_body": "الرجاء تأكيد عنوان بريدك الإلكتروني بالنقر على الزر أدناه.",
|
||||
"verify_email_email_link_text": "إليك الرابط في حال لم تكن ترغب بالنقر على أي أزرار:",
|
||||
"verify_email_email_body": "الرجاء تأكيد عنوان بريدك الإلكتروني بالنقر فوق الزر أدناه.",
|
||||
"verify_email_email_link_text": "إليك الرابط في حال لم تكن ترغب بالنقر فوق أي أزرار:",
|
||||
"email_sent": "تم إرسال الرسالة الإلكترونية بنجاح",
|
||||
"event_declined_subject": "تم الرفض: {{title}} في {{date}}",
|
||||
"event_cancelled_subject": "تم إلغاء: {{title}} في {{date}}",
|
||||
"event_cancelled_subject": "تم الإلغاء: {{title}} في {{date}}",
|
||||
"event_request_declined": "تم رفض طلب الحدث الخاص بك",
|
||||
"event_request_declined_recurring": "تم رفض طلب الحدث المتكرر الخاص بك",
|
||||
"event_request_cancelled": "تم إلغاء حدثك المُجَدْوَل",
|
||||
@@ -89,7 +89,7 @@
|
||||
"event_has_been_rescheduled": "تم التحديث - تم إعادة جدولة الحدث الخاص بك",
|
||||
"request_reschedule_subtitle": "ألغى {{organizer}} الحجز وطلب منك اختيار حجز في وقت آخر.",
|
||||
"request_reschedule_title_organizer": "لقد طلبت من {{attendee}} إعادة جدولة موعد",
|
||||
"request_reschedule_subtitle_organizer": "لقد ألغيت الحجز، ويجب أن يختار {{attendee}} وقتًا جديداً للحجز معك.",
|
||||
"request_reschedule_subtitle_organizer": "لقد ألغيت الحجز، ويجب أن يختار {{attendee}} وقتًا جديدًا للحجز معك.",
|
||||
"rescheduled_event_type_subject": "تم إرسال طلب لإعادة الجدولة: {{eventType}} مع {{name}} في {{date}}",
|
||||
"requested_to_reschedule_subject_attendee": "الإجراء المطلوب إعادة الجدولة: يُرجى حجز وقت جديد لمدة {{eventType}} مع {{name}}",
|
||||
"hi_user_name": "مرحبا {{name}}",
|
||||
@@ -129,8 +129,8 @@
|
||||
"team_upgrade_banner_description": "شكراً لك على تجربة خطة الفريق الجديدة التي نقدمها. لقد لاحظنا أن فريقك \"{{teamName}}\" بحاجة إلى الترقية.",
|
||||
"upgrade_banner_action": "قم بالترقية هنا",
|
||||
"team_upgraded_successfully": "تمت ترقية فريقك بنجاح!",
|
||||
"org_upgrade_banner_description": "شكراً لك على تجربة الخطة الجديدة لمؤسستنا التي نقدمها. لقد لاحظنا أن مؤسستك \"{{teamName}}\" بحاجة إلى الترقية.",
|
||||
"org_upgraded_successfully": "تمت ترقية مؤسستك بنجاح!",
|
||||
"org_upgrade_banner_description": "شكرًا لك على تجربة خطة Organization الجديدة التي نقدمها. لقد لاحظنا أن منظمتك \"{{teamName}}\" بحاجة إلى الترقية.",
|
||||
"org_upgraded_successfully": "تمت ترقية منظمتك بنجاح!",
|
||||
"use_link_to_reset_password": "استخدم الرابط أدناه لإعادة تعيين كلمة المرور",
|
||||
"hey_there": "مرحبًا،",
|
||||
"forgot_your_password_calcom": "نسيت كلمة المرور الخاصة بك؟ - {{appName}}",
|
||||
@@ -248,7 +248,7 @@
|
||||
"organization_general_description": "إدارة الإعدادات للغة فريقك ومنطقته الزمنية",
|
||||
"few_sentences_about_yourself": "اكتب بضع جمل عن نفسك. سيظهر هذا على صفحة عنوان URL الشخصية لديك.",
|
||||
"nearly_there": "أوشكت على الانتهاء!",
|
||||
"nearly_there_instructions": "وفي نهاية المطاف: يساعدك كتابة وصف موجز عنك ووضع صورة في الحصول على عمليات حجز وأن يعرف الأشخاص مع من سيحجزون حقًا.",
|
||||
"nearly_there_instructions": "وأخيرًا، يساعدك الوصف الموجز عنك والصورة في الحصول على الحجوزات والسماح للأشخاص بمعرفة الشخص الذين يحجزون معه.",
|
||||
"set_availability_instructions": "حدد الفترات الزمنية التي تكون متاحًا فيها بشكل متكرر. يمكنك لاحقًا تحديد المزيد منها وربطها مع تقاويم مختلفة.",
|
||||
"set_availability": "تحديد الوقت الذي تكون فيه متاحًا",
|
||||
"continue_without_calendar": "المتابعة من دون تقويم",
|
||||
@@ -304,18 +304,18 @@
|
||||
"success": "تم بنجاح",
|
||||
"failed": "فشل",
|
||||
"password_has_been_reset_login": "تمت إعادة تعيين كلمة المرور الخاصة بك. يمكنك الآن تسجيل الدخول باستخدام كلمة المرور التي تم إنشاؤها حديثًا.",
|
||||
"layout": "تخطيط",
|
||||
"layout": "التخطيط",
|
||||
"bookerlayout_default_title": "العرض الافتراضي",
|
||||
"bookerlayout_description": "يمكنك اختيار خيارات متعددة، ويمكن لمن يقومون بالحجز لديك تبديل طرق العرض.",
|
||||
"bookerlayout_user_settings_title": "تخطيط الحجز",
|
||||
"bookerlayout_user_settings_description": "يمكنك تحديد خيارات متعددة ويمكن لمن يقومون بالحجز لديك تبديل طرق العرض. يمكن إبطال هذا بحسب وضع كل فعالية.",
|
||||
"bookerlayout_user_settings_description": "يمكنك تحديد خيارات متعددة ويمكن لمن يقومون بالحجز لديك تبديل طرق العرض. يمكن تجاوز هذا على أساس كل حدث.",
|
||||
"bookerlayout_month_view": "شهر",
|
||||
"bookerlayout_week_view": "أسبوعياً",
|
||||
"bookerlayout_week_view": "أسبوعيًا",
|
||||
"bookerlayout_column_view": "عمود",
|
||||
"bookerlayout_error_min_one_enabled": "يجب تمكين تخطيط واحد على الأقل.",
|
||||
"bookerlayout_error_default_not_enabled": "التخطيط الذي حددته كطريقة عرض افتراضية ليس جزءاً من التخطيطات التي تم تمكينها.",
|
||||
"bookerlayout_error_unknown_layout": "التخطيط الذي اخترته ليس تخطيطاً صالحاً.",
|
||||
"bookerlayout_override_global_settings": "يمكنك إدارة هذا لجميع أنواع فعالياتك في <2>إعدادات/المظهر</2> أو <6>إبطال لهذا الحدث فقط</6>.",
|
||||
"bookerlayout_error_default_not_enabled": "التخطيط الذي حددته كطريقة عرض افتراضية ليس جزءًا من التخطيطات التي تم تمكينها.",
|
||||
"bookerlayout_error_unknown_layout": "التخطيط الذي اخترته ليس تخطيطًا صالحًا.",
|
||||
"bookerlayout_override_global_settings": "يمكنك إدارة هذا لجميع أنواع الأحداث لديك في <2>الإعدادات/المظهر</2> أو <6>التجاوز لهذا الحدث فقط</6>.",
|
||||
"unexpected_error_try_again": "حدث خطأ غير متوقع. حاول مجددًا.",
|
||||
"sunday_time_error": "وقت غير صالح يوم الأحد",
|
||||
"monday_time_error": "وقت غير صالح يوم الاثنين",
|
||||
@@ -337,9 +337,9 @@
|
||||
"cancelling_event_recurring": "إن هذا الحدث أحد الأمثلة على حدث متكرر.",
|
||||
"cancelling_all_recurring": "هذه هي كل الأمثلة المتبقية في الحدث المتكرر.",
|
||||
"error_with_status_code_occured": "حدث خطأ برمز الحالة {{status}}.",
|
||||
"error_event_type_url_duplicate": "يوجد بالفعل نوع فعالية مع هذا الرابط.",
|
||||
"error_event_type_unauthorized_create": "أنت غير قادر على إنشاء هذه الفعالية",
|
||||
"error_event_type_unauthorized_update": "أنت غير قادر على تعديل هذه الفعالية",
|
||||
"error_event_type_url_duplicate": "يوجد بالفعل نوع حدث بهذا الرابط.",
|
||||
"error_event_type_unauthorized_create": "أنت غير قادر على إنشاء هذا الحدث",
|
||||
"error_event_type_unauthorized_update": "أنت غير قادر على تعديل هذا الحدث",
|
||||
"error_workflow_unauthorized_create": "أنت غير قادر على إنشاء سير العمل هذا",
|
||||
"error_schedule_unauthorized_create": "أنت غير قادر على إنشاء هذا الجدول الزمني",
|
||||
"booking_already_cancelled": "تم إلغاء هذا الحجز بالفعل",
|
||||
@@ -352,7 +352,7 @@
|
||||
"no_status_bookings_yet_description": "ليست لديك عمليات حجز في حالة {{status}}. {{description}}",
|
||||
"event_between_users": "{{eventName}} بين {{host}} و{{attendeeName}}",
|
||||
"bookings": "عمليات الحجز",
|
||||
"booking_not_found": "الحجز غير موجود",
|
||||
"booking_not_found": "لم يتم العثور على الحجز",
|
||||
"bookings_description": "اطلع على الأحداث القادمة والسابقة المحجوزة لديك من خلال روابط أنواع الحدث.",
|
||||
"upcoming_bookings": "بمجرد أن يحجز شخص ما موعدًا معك، سيظهر هنا.",
|
||||
"recurring_bookings": "بمجرد أن يحجز شخص ما اجتماعًا متكررًا معك، سيظهر هنا.",
|
||||
@@ -413,8 +413,8 @@
|
||||
"team_webhooks": "فريق ويبهوكس",
|
||||
"create_new_webhook_to_account": "إنشاء ويب هوك جديد لحسابك",
|
||||
"new_webhook": "ويب هوك جديد",
|
||||
"receive_cal_meeting_data": "تلقَّ بيانات اجتماعات {{appName}} على عنوان URL محدد، وفي الوقت الفعلي، عندما تتم جدولة حدث ما أو يتم إلغاؤه.",
|
||||
"receive_cal_event_meeting_data": "تلقي بيانات اجتماعات {{appName}} على عنوان URL محدد، وفي الوقت الحقيقي، عندما تتم جدولة حدث ما أو إلغاؤه.",
|
||||
"receive_cal_meeting_data": "احصل على بيانات اجتماع {{appName}} على رابط محدد، في الوقت الفعلي، عند جدولة حدث أو إلغائه.",
|
||||
"receive_cal_event_meeting_data": "احصل على بيانات اجتماع {{appName}} على رابط محدد، في الوقت الفعلي، عند جدولة حدث أو إلغائه.",
|
||||
"responsive_fullscreen_iframe": "إطار iframe سريع الاستجابة بكامل الشاشة",
|
||||
"loading": "يجري التحميل...",
|
||||
"deleting": "جارٍ الحذف...",
|
||||
@@ -554,7 +554,7 @@
|
||||
"member": "العضو",
|
||||
"number_member_one": "{{count}} عضو",
|
||||
"number_member_other": "{{count}} من الأعضاء",
|
||||
"number_selected": "{{count}} تم اختيارها",
|
||||
"number_selected": "تم تحديد {{count}}",
|
||||
"owner": "المالك",
|
||||
"admin": "المشرف",
|
||||
"administrator_user": "المستخدم المسؤول",
|
||||
@@ -872,16 +872,16 @@
|
||||
"no_category_apps_description_automation": "إضافة تطبيق أتمتة لاستخدامه",
|
||||
"no_category_apps_description_other": "أضف أي نوع آخر من التطبيقات للقيام بأي شيء",
|
||||
"no_category_apps_description_web3": "إضافة تطبيق web3 لصفحات الحجز لديك",
|
||||
"no_category_apps_description_messaging": "إضافة تطبيق مراسلة لإعداد التنبيهات والمذكرات المخصصة",
|
||||
"no_category_apps_description_messaging": "إضافة تطبيق مراسلة لإعداد الإشعارات والتذكيرات المخصصة",
|
||||
"no_category_apps_description_crm": "إضافة تطبيق CRM لتتبع من قابلته",
|
||||
"installed_app_calendar_description": "قم بتعيين التقويمات للتحقق من وجود تعارضات لمنع الحجوزات المزدوجة.",
|
||||
"installed_app_payment_description": "قم بإعداد أي خدمات معالجة دفع تود استخدامها عند التحصيل من العملاء.",
|
||||
"installed_app_analytics_description": "تهيئة أي تطبيقات تستخدم لصفحات الحجز الخاصة بك",
|
||||
"installed_app_other_description": "جميع تطبيقاتك المثبتة من الفئات الأخرى.",
|
||||
"installed_app_conferencing_description": "تهيئة أي تطبيقات المؤتمرات لاستخدامها",
|
||||
"installed_app_conferencing_description": "تكوين تطبيقات المؤتمرات المراد استخدامها",
|
||||
"installed_app_automation_description": "تهيئة تطبيقات الأتمتة التي سيتم استخدامها",
|
||||
"installed_app_web3_description": "تهيئة تطبيقات web3 التي يمكن استخدامها لصفحات الحجز لديك",
|
||||
"installed_app_messaging_description": "تكوين أي تطبيقات الرسائل لاستخدامها لإعداد الإشعارات والمذكرات المخصصة",
|
||||
"installed_app_messaging_description": "تكوين تطبيقات المراسلة المراد استخدامها لإعداد الإشعارات والتذكيرات المخصصة",
|
||||
"installed_app_crm_description": "تهيئة تطبيقات CRM التي يمكن استخدامها لتتبع من قابلته",
|
||||
"analytics": "التحليلات",
|
||||
"empty_installed_apps_headline": "لم يتم تثبيت أي تطبيق",
|
||||
@@ -908,7 +908,7 @@
|
||||
"create_events_on": "إنشاء أحداث في",
|
||||
"enterprise_license": "هذه هي ميزة للمؤسسات",
|
||||
"enterprise_license_description": "لتمكين هذه الميزة، احصل على مفتاح نشر في وحدة التحكم {{consoleUrl}} وأضفه إلى وحدة التحكم الخاصة بك. nv باسم CALCOM_LICENSE_KEY. إذا كان لدى فريقك بالفعل ترخيص، يرجى الاتصال بـ {{supportMail}} للحصول على المساعدة.",
|
||||
"enterprise_license_development": "يمكنك اختبار هذه الميزة في وضع التطوير. لاستخدام الإنتاج يرجى جعل المسؤول يذهب إلى <2>/auth/setup</2> لإدخال مفتاح الترخيص.",
|
||||
"enterprise_license_development": "يمكنك اختبار هذه الميزة في وضع التطوير. لاستخدام الإنتاج، يرجى مطالبة المسؤول بالذهاب إلى <2>/auth/setup</2> لإدخال مفتاح الترخيص.",
|
||||
"missing_license": "الترخيص مفقود",
|
||||
"signup_requires": "يلزم ترخيص تجاري",
|
||||
"signup_requires_description": "لا تقدم شركة {{companyName}} حاليًا إصدارًا مجانيًا مفتوح المصدر لصفحة التسجيل. للحصول على حق الوصول الكامل إلى مكونات التسجيل، يجب أن تحصل على ترخيص تجاري. للاستخدام الشخصي، نوصي باستخدام منصة Prisma Data أو أي واجهة أخرى من واجهات Postgres لإنشاء الحسابات.",
|
||||
@@ -1001,9 +1001,9 @@
|
||||
"user_impersonation_heading": "انتحال شخصية مستخدم",
|
||||
"user_impersonation_description": "يسمح لفريق الدعم الخاص بنا بتسجيل الدخول مؤقتًا نيابة عنك لمساعدتنا على حل أي مشاكل تبلغ عنها بسرعة.",
|
||||
"team_impersonation_description": "يسمح لمالكي/مشرفي فريقك بتسجيل الدخول مؤقتًا نيابة عنك.",
|
||||
"make_team_private": "اجعل الفريق سري",
|
||||
"make_team_private_description": "لن يتمكن أعضاء فريقك من رؤية أعضاء الفريق الآخرين عند تشغيل هذا.",
|
||||
"you_cannot_see_team_members": "لا يمكنك رؤية جميع أعضاء فريق سري.",
|
||||
"make_team_private": "اجعل الفريق خاصًا",
|
||||
"make_team_private_description": "لن يتمكن أعضاء فريقك من رؤية أعضاء الفريق الآخرين عند تشغيل هذا الإعداد.",
|
||||
"you_cannot_see_team_members": "لا يمكنك رؤية جميع أعضاء الفريق السري.",
|
||||
"allow_booker_to_select_duration": "السماح لمن يقوم بالحجز بتحديد المدة",
|
||||
"impersonate_user_tip": "يتم مراجعة جميع استخدامات هذه الميزة.",
|
||||
"impersonating_user_warning": "انتحال اسم مستخدم \"{{user}}\".",
|
||||
@@ -1071,18 +1071,18 @@
|
||||
"event_location": "موقع الحدث",
|
||||
"reschedule_optional": "سبب إعادة الجدولة (اختياري)",
|
||||
"reschedule_placeholder": "أخبر الآخرين لماذا تحتاج إلى إعادة جدولة",
|
||||
"event_cancelled": "تم إلغاء هذه الفعالية",
|
||||
"event_cancelled": "تم إلغاء هذا الحدث",
|
||||
"emailed_information_about_cancelled_event": "لقد أرسلنا إليك وإلى الحاضرين الآخرين بريداً إلكترونياً لإعلامهم.",
|
||||
"this_input_will_shown_booking_this_event": "سيُعرَض هذا المُدخَل عند حجز هذا الحدث",
|
||||
"meeting_url_in_conformation_email": "عنوان رابط الاجتماع موجود في رسالة التأكيد الإلكترونية",
|
||||
"url_start_with_https": "يجب أن يبدأ العنوان بـ http:// أو https://",
|
||||
"number_provided": "سيتم توفير رقم الهاتف",
|
||||
"before_event_trigger": "قبل بدء الحدث",
|
||||
"event_cancelled_trigger": "متى تم إلغاء هذه الفعالية",
|
||||
"event_cancelled_trigger": "متى تم إلغاء هذا الحدث",
|
||||
"new_event_trigger": "متى تم حجز الحدث الجديد",
|
||||
"email_host_action": "إرسال رسالة إلكترونية إلى المضيف",
|
||||
"email_attendee_action": "إرسال رسالة إلكترونية إلى الحضور",
|
||||
"sms_attendee_action": "إرسال رسالة نصية قصيرة إلى واحد من الحضور",
|
||||
"sms_attendee_action": "إرسال رسالة نصية قصيرة إلى أحد الحضور",
|
||||
"sms_number_action": "إرسال رسالة نصية قصيرة إلى رقم محدد",
|
||||
"workflows": "سير العمل",
|
||||
"new_workflow_btn": "سير عمل جديد",
|
||||
@@ -1331,9 +1331,9 @@
|
||||
"event_one": "حدث",
|
||||
"event_other": "أحداث",
|
||||
"profile_team_description": "إدارة إعدادات الملف الشخصي لفريقك",
|
||||
"profile_org_description": "إدارة الإعدادات لملف مؤسستك",
|
||||
"profile_org_description": "إدارة الإعدادات لملف منظمتك",
|
||||
"members_team_description": "المستخدمين الموجودين في المجموعة",
|
||||
"organization_description": "إدارة المشرفين والأعضاء في مؤسستك",
|
||||
"organization_description": "إدارة المسؤولين والأعضاء في منظمتك",
|
||||
"team_url": "رابط الفريق",
|
||||
"team_members": "أعضاء الفريق",
|
||||
"more": "المزيد",
|
||||
@@ -1479,10 +1479,10 @@
|
||||
"yearly": "سنويًا",
|
||||
"checkout": "الدفع",
|
||||
"your_team_disbanded_successfully": "تم حل فريقك بنجاح",
|
||||
"your_org_disbanded_successfully": "تم حل مؤسستك بنجاح",
|
||||
"your_org_disbanded_successfully": "تم حل منظمتك بنجاح",
|
||||
"error_creating_team": "خطأ في إنشاء فريق",
|
||||
"you": "أنت",
|
||||
"resend_email": "إعادة إرسال رسالة إلكترونية",
|
||||
"resend_email": "إعادة إرسال رسالة البريد الإلكتروني",
|
||||
"member_already_invited": "تمت دعوة العضو بالفعل",
|
||||
"enter_email_or_username": "أدخل بريد إلكتروني أو اسم مستخدم",
|
||||
"team_name_taken": "هذا الاسم مأخوذ بالفعل",
|
||||
@@ -1548,7 +1548,7 @@
|
||||
"no_apps": "لا توجد تطبيقات مفعلة لهذا المثال من Cal",
|
||||
"no_apps_configured": "لم يتم تكوين أي تطبيق بعد",
|
||||
"enable_in_settings": "يمكنك تمكين التطبيقات في الإعدادات",
|
||||
"please_contact_admin": "الرجاء الاتصال بالمشرف",
|
||||
"please_contact_admin": "يرجى الاتصال بالمسؤول",
|
||||
"apps_settings": "إعدادات التطبيقات",
|
||||
"fill_this_field": "يرجى ملء هذا الحقل",
|
||||
"options": "خيارات",
|
||||
@@ -1652,10 +1652,10 @@
|
||||
"delete_sso_configuration_confirmation_description": "هل تريد بالتأكيد حذف تكوين {{connectionType}}؟ لن يتمكن أعضاء فريقك الذين يستخدمون معلومات تسجيل الدخول إلى {{connectionType}} من الوصول إلى Cal.com بعد الآن.",
|
||||
"organizer_timezone": "منظم المناطق الزمنية",
|
||||
"email_user_cta": "عرض الدعوة",
|
||||
"email_no_user_invite_heading": "تم دعوتك للانضمام إلى {{appName}} {{entity}}",
|
||||
"email_no_user_invite_heading": "تمت دعوتك للانضمام إلى {{appName}} {{entity}}",
|
||||
"email_no_user_invite_subheading": "دعاك {{invitedBy}} للانضمام إلى فريقه على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت وفريقك من جدولة الاجتماعات دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
|
||||
"email_user_invite_subheading": "دعاك {{invitedBy}} للانضمام إلى فريقه {{teamName}} لدى {{entity}} على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت و{{entity}} من جدولة الاجتماعات من دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
|
||||
"email_no_user_invite_steps_intro": "سنرشدك خلال بضع خطوات قصيرة وستستمتع بجدولة سريعة خالية من الإجهاد مع {{entity}}.",
|
||||
"email_user_invite_subheading": "لقد دعاك {{invitedBy}} للانضمام إلى {{entity}} `{{teamName}}` على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت و{{entity}} من جدولة الاجتماعات دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
|
||||
"email_no_user_invite_steps_intro": "سنرشدك خلال بضع خطوات قصيرة وستستمتع بجدول زمني خالٍ من التوتر مع {{entity}} في لمح البصر.",
|
||||
"email_no_user_step_one": "اختر اسم المستخدم الخاص بك",
|
||||
"email_no_user_step_two": "ربط حساب التقويم الخاص بك",
|
||||
"email_no_user_step_three": "تعيين أوقاتك المتاحة",
|
||||
@@ -1666,8 +1666,8 @@
|
||||
"scheduler": "{Scheduler}",
|
||||
"no_workflows": "لا يوجد أي سير عمل",
|
||||
"change_filter": "قم بتغيير عامل التصفية لمشاهدة سير العمل الشخصي وفريقك.",
|
||||
"change_filter_common": "تغيير عامل الفرز لرؤية النتائج.",
|
||||
"no_results_for_filter": "لا توجد نتائج لعامل الفرز",
|
||||
"change_filter_common": "تغيير عامل التصفية لرؤية النتائج.",
|
||||
"no_results_for_filter": "لا توجد نتائج لعامل التصفية",
|
||||
"recommended_next_steps": "الخطوات التالية الموصى بها",
|
||||
"create_a_managed_event": "إنشاء نوع من الأحداث المُدارة",
|
||||
"meetings_are_better_with_the_right": "الاجتماعات أفضل مع وجود أعضاء الفريق المناسبين. قم بدعوتهم الآن.",
|
||||
@@ -1678,7 +1678,7 @@
|
||||
"no_longer_attending": "لن تحضر هذا الحدث",
|
||||
"attendee_no_longer_attending_subject": "أحد الحضور لن يحضر {{title}} في {{date}}",
|
||||
"attendee_no_longer_attending": "أحد الحضور لن يحضر الحدث الخاص بك",
|
||||
"attendee_no_longer_attending_subtitle": "تم إلغاء الحدث {{name}}، وهو ما يعني توافر معقد شاغر في هذا التوقيت",
|
||||
"attendee_no_longer_attending_subtitle": "تم إلغاء {{name}}. هذا يعني أنه تم فتح مقعد لهذه الفترة الزمنية",
|
||||
"create_event_on": "إنشاء حدث في",
|
||||
"create_routing_form_on": "إنشاء نموذج توجيه على",
|
||||
"default_app_link_title": "تعيين رابط تطبيق افتراضي",
|
||||
@@ -1690,7 +1690,7 @@
|
||||
"booking_questions_title": "أسئلة حول الحجز",
|
||||
"booking_questions_description": "يمكنك تخصيص الأسئلة المطروحة في صفحة الحجز",
|
||||
"add_a_booking_question": "اطرح سؤالاً",
|
||||
"identifier": "مُعرِّف",
|
||||
"identifier": "المُعرِّف",
|
||||
"duplicate_email": "البريد الإلكتروني مكرر",
|
||||
"booking_with_payment_cancelled": "لم يعد الدفع لهذا الحدث ممكنًا",
|
||||
"booking_with_payment_cancelled_already_paid": "استرداد المبلغ المدفوع لهذا الحجز قيد التنفيذ.",
|
||||
@@ -1733,14 +1733,14 @@
|
||||
"no_responses_yet": "لا توجد ردود بعد",
|
||||
"no_routes_defined": "لا توجد مسارات محددة",
|
||||
"this_will_be_the_placeholder": "سيكون هذا هو العنصر النائب",
|
||||
"error_booking_event": "حدث خطأ عند حجز الفعالية، الرجاء تحديث الصفحة والمحاولة ثانيةً",
|
||||
"error_booking_event": "حدث خطأ عند حجز الحدث، يرجى تحديث الصفحة والمحاولة ثانيةً",
|
||||
"timeslot_missing_title": "لم يتم تحديد فترة زمنية",
|
||||
"timeslot_missing_description": "يرجى تحديد فترة زمنية لحجز الحدث.",
|
||||
"timeslot_missing_cta": "حدد فترة زمنية",
|
||||
"switch_monthly": "التبديل إلى العرض الشهري",
|
||||
"switch_weekly": "التبديل إلى العرض الأسبوعي",
|
||||
"switch_multiday": "التبديل إلى العرض اليومي",
|
||||
"switch_columnview": "التبديل إلى العرض العمود",
|
||||
"switch_columnview": "التبديل إلى عرض العمود",
|
||||
"num_locations": "{{num}} من خيارات الموقع",
|
||||
"select_on_next_step": "حدد في الخطوة التالية",
|
||||
"this_meeting_has_not_started_yet": "لم يبدأ هذا الاجتماع بعد",
|
||||
@@ -1795,7 +1795,7 @@
|
||||
"least_booked_members": "الأعضاء الأقل حجزًا",
|
||||
"events_created": "تم إنشاء الأحداث",
|
||||
"events_completed": "تم اكتمال الأحداث",
|
||||
"events_cancelled": "تم إلغاء الفعاليات",
|
||||
"events_cancelled": "تم إلغاء الأحداث",
|
||||
"events_rescheduled": "تمت إعادة جدولة الأحداث",
|
||||
"from_last_period": "من آخر فترة",
|
||||
"from_to_date_period": "من: {{startDate}} إلى: {{endDate}}",
|
||||
@@ -1834,13 +1834,13 @@
|
||||
"invite_link_copied": "تم نسخ رابط الدعوة",
|
||||
"invite_link_deleted": "تم حذف رابط الدعوة",
|
||||
"invite_link_updated": "تم حفظ إعدادات رابط الدعوة",
|
||||
"link_expires_after": "روابط تم تعيينها للانتهاء بعد...",
|
||||
"link_expires_after": "تم تعيين الروابط للانتهاء بعد...",
|
||||
"one_day": "1 يوم",
|
||||
"seven_days": "7 أيام",
|
||||
"thirty_days": "30 يوم",
|
||||
"thirty_days": "30 يومًا",
|
||||
"three_months": "3 أشهر",
|
||||
"one_year": "1 سنة",
|
||||
"team_invite_received": "تم دعوتك للانضمام إلى {{teamName}}",
|
||||
"team_invite_received": "تمت دعوتك للانضمام إلى {{teamName}}",
|
||||
"currency_string": "{{amount, currency}}",
|
||||
"charge_card_dialog_body": "أنت على وشك تحصيل مبلغ {{amount, currency}} من أحد الحضور. هل أنت متأكد من أنك تريد المتابعة؟",
|
||||
"charge_attendee": "تحصيل مبلغ {{amount, currency}} من أحد الحضور",
|
||||
@@ -1849,7 +1849,7 @@
|
||||
"email_invite_team_bulk": "تم دعوة {{userCount}} من المستخدمين",
|
||||
"error_collecting_card": "خطأ في التحصيل من البطاقة",
|
||||
"image_size_limit_exceed": "يجب ألا يتجاوز حجم الصورة التي تم تحميلها 5 ميغابايت",
|
||||
"unauthorized_workflow_error_message": "{{errorCode}}: أنت غير مخوّل لك بتمكين أو تعطيل سير العمل هذا",
|
||||
"unauthorized_workflow_error_message": "{{errorCode}}: غير مصرح لك بتمكين سير العمل هذا أو تعطيله",
|
||||
"inline_embed": "التضمين",
|
||||
"load_inline_content": "تحميل نوع الحدث مع محتوى آخر من موقعكبشكل مباشر.",
|
||||
"floating_pop_up_button": "زر منبثق عائم",
|
||||
@@ -1871,66 +1871,66 @@
|
||||
"first_event_type_webhook_description": "قم بإنشاء أول شبكة ويب هوك لهذا النوع من الأحداث",
|
||||
"install_app_on": "تثبيت التطبيق على",
|
||||
"create_for": "إنشاء من أجل",
|
||||
"setup_organization": "إعداد مؤسسة",
|
||||
"organization_banner_description": "إنشاء بيئات حيث يمكن لفرقك إنشاء تطبيقات مشتركة، ومسارات عمل وأنواع فعاليات مع جدولة دورية وجماعية.",
|
||||
"organization_banner_title": "إدارة المنظمات مع فرق متعددة",
|
||||
"setup_organization": "إعداد منظمة",
|
||||
"organization_banner_description": "إنشاء بيئات حيث يمكن لفرقك إنشاء تطبيقات مشتركة ومهام سير العمل وأنواع الأحداث باستخدام الجدولة الدوارة والجماعية.",
|
||||
"organization_banner_title": "إدارة المنظمات ذات الفرق المتعددة",
|
||||
"set_up_your_organization": "إعداد منظمتك",
|
||||
"organizations_description": "المنظمات هي بيئات مشتركة حيث يمكن للفرق إنشاء أنواع الفعاليات المشتركة والتطبيقات ومسارات العمل والمزيد.",
|
||||
"organizations_description": "المنظمات هي بيئات مشتركة حيث يمكن للفرق إنشاء أنواع الأحداث المشتركة والتطبيقات ومهام سير العمل والمزيد.",
|
||||
"must_enter_organization_name": "يجب إدخال اسم المنظمة",
|
||||
"must_enter_organization_admin_email": "يجب إدخال عنوان البريد الإلكتروني لمؤسستك",
|
||||
"admin_email": "عنوان البريد الإلكتروني لمؤسستك",
|
||||
"must_enter_organization_admin_email": "يجب إدخال عنوان البريد الإلكتروني لمنظمتك",
|
||||
"admin_email": "عنوان البريد الإلكتروني لمنظمتك",
|
||||
"admin_username": "اسم المستخدم للمسؤول",
|
||||
"organization_name": "اسم المنظمة",
|
||||
"organization_url": "رابط المنظمة",
|
||||
"organization_verify_header": "تأكيد البريد الإلكتروني لمؤسستك",
|
||||
"organization_verify_email_body": "الرجاء استخدام الرمز أدناه لتأكيد عنوان بريدك الإلكتروني لمواصلة إعداد مؤسستك.",
|
||||
"additional_url_parameters": "مقاييس URL إضافية",
|
||||
"about_your_organization": "حول مؤسستك",
|
||||
"about_your_organization_description": "المنظمات هي بيئات مشتركة فيها يمكنك إنشاء فرق متعدد مع أعضاء مشتركين، وأنواع الفعاليات، والتطبيقات، ومسارات العمل والمزيد.",
|
||||
"organization_verify_header": "تأكيد البريد الإلكتروني لمنظمتك",
|
||||
"organization_verify_email_body": "الرجاء استخدام الرمز أدناه لتأكيد عنوان بريدك الإلكتروني لمواصلة إعداد منظمتك.",
|
||||
"additional_url_parameters": "معلمات الرابط الإضافية",
|
||||
"about_your_organization": "حول منظمتك",
|
||||
"about_your_organization_description": "المنظمات هي بيئات مشتركة حيث يمكنك إنشاء فرق متعددة مع أعضاء مشتركين وأنواع الأحداث والتطبيقات ومهام سير العمل والمزيد.",
|
||||
"create_your_teams": "إنشاء فرقك",
|
||||
"create_your_teams_description": "البدء في الجدولة معاً بإضافة أعضاء فريقك إلى مؤسستك",
|
||||
"invite_organization_admins": "قم بدعوة مشرفي مؤسستك",
|
||||
"invite_organization_admins_description": "سيتمكن هؤلاء المشرفون من الوصول إلى جميع الفرق في مؤسستك. يمكنك إضافة مشرفين وأعضاء في وقت لاحق.",
|
||||
"create_your_teams_description": "ابدأوا الجدولة معًا عن طريق إضافة أعضاء فريقك إلى منظمتك",
|
||||
"invite_organization_admins": "دعوة مشرفي منظمتك",
|
||||
"invite_organization_admins_description": "سيتمكن هؤلاء المسؤولون من الوصول إلى جميع الفرق في منظمتك. يمكنك إضافة مسؤولي الفريق والأعضاء لاحقًا.",
|
||||
"set_a_password": "تعيين كلمة مرور",
|
||||
"set_a_password_description": "سيؤدي هذا إلى إنشاء حساب مستخدم جديد مع البريد الإلكتروني لمؤسستك وكلمة المرور هذه.",
|
||||
"set_a_password_description": "سيؤدي هذا إلى إنشاء حساب مستخدم جديد بالبريد الإلكتروني لمنظمتك وكلمة المرور هذه.",
|
||||
"organization_logo": "شعار المنظمة",
|
||||
"organization_about_description": "بضع جمل عن منظمتك. ستظهر على الصفحة المتاحجة للعموم لمنظمتك.",
|
||||
"ill_do_this_later": "سأفعل هذا لاحقاً",
|
||||
"organization_about_description": "بضع جمل عن منطمتك. سيظهر هذا في صفحة الملف الشخصي العامة لمنظمتك.",
|
||||
"ill_do_this_later": "سأفعل هذا لاحقًا",
|
||||
"verify_your_email": "تأكيد بريدك الإلكتروني",
|
||||
"enter_digit_code": "أدخل الرمز المكون من 6 أرقام الذي أرسلناه إلى {{email}}",
|
||||
"verify_email_organization": "تأكيد بريدك الإلكتروني لإنشاء مؤسسة",
|
||||
"verify_email_organization": "أكد بريدك الإلكتروني لإنشاء منظمة",
|
||||
"code_provided_invalid": "الرمز المقدم غير صالح، حاول ثانيةً",
|
||||
"email_already_used": "البريد الإلكتروني مستخدم بالفعل",
|
||||
"organization_admin_invited_heading": "تم دعوتك للانضمام إلى {{orgName}}",
|
||||
"organization_admin_invited_body": "انضم إلى فريقك في {{orgName}} وابدأ بالتركيز على الاجتماع، وليس عقد الاجتماعات!",
|
||||
"duplicated_slugs_warning": "لا يمكن إنشاء الفرق التالية بسبب التكرار: {{slugs}}",
|
||||
"organization_admin_invited_heading": "تمت دعوتك للانضمام إلى {{orgName}}",
|
||||
"organization_admin_invited_body": "انضم إلى فريقك في {{orgName}} وابدأ الانشغال بالاجتماعاتك، وليس بكيفية الإعداد لها!",
|
||||
"duplicated_slugs_warning": "لا يمكن إنشاء الفرق التالية بسبب تكرار الكلمات اللطيفة (Slug): {{slugs}}",
|
||||
"team_names_empty": "لا يمكن أن تكون أسماء الفرق فارغة",
|
||||
"team_names_repeated": "لا يمكن تكرار أسماء الفرق",
|
||||
"user_belongs_organization": "المستخدم ينتمي إلى منظمة",
|
||||
"org_no_teams_yet": "هذه المنظمة ليس لديها فرق بعد",
|
||||
"org_no_teams_yet_description": "إذا كنت مسؤول، تأكد من إنشاء فرق لعرضها هنا.",
|
||||
"set_up": "إعداد",
|
||||
"set_up_your_profile": "إعداد ملفك التعريفي",
|
||||
"set_up": "الإعداد",
|
||||
"set_up_your_profile": "إعداد ملفك الشخصي",
|
||||
"set_up_your_profile_description": "دع الناس يعرفون من أنت داخل {{orgName}}، ومتى يتعاملون مع الرابط العام الخاص بك.",
|
||||
"my_profile": "ملفي التعريفي",
|
||||
"my_settings": "إعداداتي",
|
||||
"my_profile": "الملف الشخصي",
|
||||
"my_settings": "الإعدادات",
|
||||
"crm": "CRM",
|
||||
"messaging": "المراسلة",
|
||||
"sender_id_info": "الاسم أو الرقم المبين كمرسل لرسالة نصية قصيرة (بعض البلدان لا تسمح بأن تكون هويات المرسل أبجدية رقمية)",
|
||||
"org_admins_can_create_new_teams": "يمكن فقط لمشرف مؤسستك إنشاء فرق جديدة",
|
||||
"sender_id_info": "الاسم أو الرقم الظاهر على أنه مرسل الرسالة النصية القصيرة (بعض البلدان لا تسمح بمعرفات المرسل الأبجدية الرقمية)",
|
||||
"org_admins_can_create_new_teams": "يمكن فقط لمسؤول منظمتك إنشاء فرق جديدة",
|
||||
"google_new_spam_policy": "يمكن أن تمنعك سياسة Google الجديدة للرسائل غير المرغوب فيها من تلقي أي إشعارات بالبريد الإلكتروني والتقويم حول هذا الحجز.",
|
||||
"resolve": "حل",
|
||||
"no_organization_slug": "حدث خطأ أثناء إنشاء فرق لهذه المؤسسة. ربط الرابط مفقود.",
|
||||
"no_organization_slug": "حدث خطأ أثناء إنشاء فرق لهذه المنظمة. الكلمة اللطيفة للرابط مفقودة.",
|
||||
"org_name": "اسم المنظمة",
|
||||
"org_url": "رابط المنظمة",
|
||||
"copy_link_org": "نسخ الرابط إلى المنظمة",
|
||||
"404_the_org": "المنظمة",
|
||||
"404_the_team": "الفريق",
|
||||
"404_claim_entity_org": "المطالبة بنطاقك الفرعي لمؤسستك",
|
||||
"404_claim_entity_org": "المطالبة بنطاقك الفرعي لمنظمتك",
|
||||
"404_claim_entity_team": "المطالبة بهذا الفريق والبدء في إدارة الجداول الزمنية بشكل جماعي",
|
||||
"insights_all_org_filter": "الكل",
|
||||
"insights_team_filter": "الفريق: {{teamName}}",
|
||||
"insights_user_filter": "المستخدم: {{userName}}",
|
||||
"insights_subtitle": "عرض رؤى الحجز عبر فعالياتك",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف خيوطك الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
"insights_subtitle": "عرض Insights الحجز عبر أحداثك",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"verify_email_email_link_text": "Zde je odkaz pro případ, že nemáte rádi klikání na tlačítka:",
|
||||
"email_sent": "E-mail byl odeslán",
|
||||
"event_declined_subject": "Odmítnuto: {{title}} v {{date}}",
|
||||
"event_cancelled_subject": "Zrušeno: {{title}} ({{date}})",
|
||||
"event_cancelled_subject": "Zrušeno: {{title}} dne {{date}}",
|
||||
"event_request_declined": "Žádost o událost byla zamítnuta",
|
||||
"event_request_declined_recurring": "Žádost o opakovanou událost byla zamítnuta",
|
||||
"event_request_cancelled": "Vaše plánovaná událost byla zrušena",
|
||||
@@ -89,7 +89,7 @@
|
||||
"event_has_been_rescheduled": "Změna - Vaše událost byla přesunuta na jindy",
|
||||
"request_reschedule_subtitle": "{{organizer}} zrušil rezervaci a požádal vás, abyste si vybrali jiný termín.",
|
||||
"request_reschedule_title_organizer": "Požádali jste uživatele {{attendee}} o změnu časového plánu",
|
||||
"request_reschedule_subtitle_organizer": "Zrušili jste rezervaci a účastník {{attendee}} by si s vámi měl rezervovat nový čas.",
|
||||
"request_reschedule_subtitle_organizer": "Zrušili jste rezervaci a účastník {{attendee}} by si s vámi měl zarezervovat nový čas.",
|
||||
"rescheduled_event_type_subject": "Žádost o přesunutí schůzky: {{eventType}} s {{name}} dne {{date}}",
|
||||
"requested_to_reschedule_subject_attendee": "Akce vyžadovaná k přeplánování: zarezervujte si nový čas pro událost {{eventType}} s osobou {{name}}",
|
||||
"hi_user_name": "Hezký den, {{name}}",
|
||||
@@ -129,7 +129,7 @@
|
||||
"team_upgrade_banner_description": "Děkujeme vám za vyzkoušení našeho nového týmového plánu. Všimli jsme si, že váš tým „{{teamName}}“ je nutné upgradovat.",
|
||||
"upgrade_banner_action": "Upgradovat zde",
|
||||
"team_upgraded_successfully": "Váš tým byl úspěšně přešel na vyšší verzi!",
|
||||
"org_upgrade_banner_description": "Děkujeme, že jste vyzkoušeli tarif naší organizace. Všimli jsme si, že vaše organizace „{{teamName}}“ vyžaduje upgrade.",
|
||||
"org_upgrade_banner_description": "Děkujeme, že jste vyzkoušeli náš tarif Organizace. Všimli jsme si, že vaše organizace „{{teamName}}“ vyžaduje upgrade.",
|
||||
"org_upgraded_successfully": "Vaše organizace byla upgradována!",
|
||||
"use_link_to_reset_password": "Pro obnovení hesla použijte odkaz níž",
|
||||
"hey_there": "Zdravíme,",
|
||||
@@ -248,7 +248,7 @@
|
||||
"organization_general_description": "Správa nastavení jazyka a časového pásma vašeho týmu",
|
||||
"few_sentences_about_yourself": "Pár vět o vás. Budou se zobrazovat na URL vaší osobní stránky.",
|
||||
"nearly_there": "Téměř hotovo!",
|
||||
"nearly_there_instructions": "Poslední věc... stručný popis o vás a fotografie vám pomůžou získat rezervace a lidi aspoň budou vědět, koho si rezervují.",
|
||||
"nearly_there_instructions": "Poslední věc... pár slov o vás a fotografie vám pomůžou získat rezervace a lidi aspoň budou vědět, koho si rezervují.",
|
||||
"set_availability_instructions": "Určete si časové intervaly, kdy jste pravidelně dostupní. Později můžete vytvořit další a přiřadit je různým kalendářům.",
|
||||
"set_availability": "Nastavte, jak jste dostupní",
|
||||
"continue_without_calendar": "Pokračovat bez kalendáře",
|
||||
@@ -314,8 +314,8 @@
|
||||
"bookerlayout_column_view": "Sloupec",
|
||||
"bookerlayout_error_min_one_enabled": "Musí být povoleno alespoň jedno rozvržení.",
|
||||
"bookerlayout_error_default_not_enabled": "Rozvržení, které jste vybrali jako výchozí zobrazení, není součástí povolených rozvržení.",
|
||||
"bookerlayout_error_unknown_layout": "Vámi vybrané rozvržení není platné rozvržení.",
|
||||
"bookerlayout_override_global_settings": "Tohle lze nastavit pro všechny typy událostí v <2>nastavení / vzhledu</2> nebo <6>nadefinovat pouze pro tuto událost</6>.",
|
||||
"bookerlayout_error_unknown_layout": "Vámi vybrané rozvržení není platné.",
|
||||
"bookerlayout_override_global_settings": "Toto lze nastavit pro všechny typy událostí v <2>nastavení / vzhled</2> nebo <6>nadefinovat pouze pro tuto událost</6>.",
|
||||
"unexpected_error_try_again": "Došlo k neočekávané chybě. Zkuste to znovu.",
|
||||
"sunday_time_error": "Neplatný čas v neděli",
|
||||
"monday_time_error": "Neplatný čas v pondělí",
|
||||
@@ -341,7 +341,7 @@
|
||||
"error_event_type_unauthorized_create": "Tuto událost nelze vytvořit",
|
||||
"error_event_type_unauthorized_update": "Tuto událost nelze upravit",
|
||||
"error_workflow_unauthorized_create": "Tento pracovní postup nelze vytvořit",
|
||||
"error_schedule_unauthorized_create": "Nejste schopni vytvořit tento plán",
|
||||
"error_schedule_unauthorized_create": "Tento plán nelze vytvořit",
|
||||
"booking_already_cancelled": "Tato rezervace již byla zrušena",
|
||||
"booking_already_accepted_rejected": "Tato rezervace již byla přijata nebo zamítnuta",
|
||||
"go_back_home": "Přejít na úvod",
|
||||
@@ -352,7 +352,7 @@
|
||||
"no_status_bookings_yet_description": "Nemáte žádné {{status}} rezervace. {{description}}",
|
||||
"event_between_users": "{{eventName}} mezi {{host}} a {{attendeeName}}",
|
||||
"bookings": "Rezervace",
|
||||
"booking_not_found": "Rezervace nenalezena",
|
||||
"booking_not_found": "Rezervace nebyla nalezena",
|
||||
"bookings_description": "Zobrazte si nadcházející a proběhlé události rezervovaní prostřednictvím odkazu na typ události.",
|
||||
"upcoming_bookings": "Jakmile si někdo rezervuje váš čas, zobrazí se to zde.",
|
||||
"recurring_bookings": "Jakmile si někdo s vámi zarezervuje opakovanou schůzku, zobrazí se to zde.",
|
||||
@@ -398,12 +398,12 @@
|
||||
"browse_api_documentation": "Procházejte naši API dokumentaci",
|
||||
"leverage_our_api": "Pro plnou kontrolu a přizpůsobení využijte naše API.",
|
||||
"create_webhook": "Vytvořit webhook",
|
||||
"booking_cancelled": "Rezervace zrušena",
|
||||
"booking_cancelled": "Rezervace byla zrušena",
|
||||
"booking_rescheduled": "Rezervace přesunuta",
|
||||
"recording_ready": "Odkaz ke stažení záznamu je připraven",
|
||||
"booking_created": "Rezervace vytvořena",
|
||||
"booking_rejected": "Rezervace zamítnuta",
|
||||
"booking_requested": "Rezervace vyžádána",
|
||||
"booking_rejected": "Rezervace byla zamítnuta",
|
||||
"booking_requested": "Byla vyžádána rezervace",
|
||||
"meeting_ended": "Schůzka skončila",
|
||||
"form_submitted": "Formulář byl odeslán",
|
||||
"event_triggers": "Eventy na základě akce",
|
||||
@@ -867,7 +867,7 @@
|
||||
"no_category_apps": "Žádné aplikace v kategorii {{category}}",
|
||||
"all_apps": "Všechny aplikace",
|
||||
"no_category_apps_description_calendar": "Přidejte aplikaci kalendáře, ať máte přehled v zájmu prevence dvojích rezervací",
|
||||
"no_category_apps_description_conferencing": "Zkuste přidat aplikaci pro videokonference s klienty.",
|
||||
"no_category_apps_description_conferencing": "Zkuste přidat aplikaci pro videokonference s klienty",
|
||||
"no_category_apps_description_payment": "Přidejte platební aplikaci, která usnadní provádění transakcí mezi vámi a vašimi klienty",
|
||||
"no_category_apps_description_analytics": "Přidejte analytickou aplikaci pro vaše rezervační stránky",
|
||||
"no_category_apps_description_automation": "Přidejte aplikaci pro automatizaci",
|
||||
@@ -1002,8 +1002,8 @@
|
||||
"user_impersonation_heading": "Přizpůsobení uživatele",
|
||||
"user_impersonation_description": "Náš tým podpory se bude moci dočasně přihlásit místo vás, aby nám pomohl rychle vyřešit jakékoli problémy, které nám nahlásíte.",
|
||||
"team_impersonation_description": "Umožňuje vašim vlastníkům/správcům týmu dočasně se přihlásit jako vy.",
|
||||
"make_team_private": "Soukromý tým",
|
||||
"make_team_private_description": "Po zapnutí této funkce nebudou členové vašeho týmu vidět ostatní členy týmu.",
|
||||
"make_team_private": "Nastavit tým jako soukromý",
|
||||
"make_team_private_description": "Po zapnutí této funkce neuvidí členové vašeho týmu ostatní členy týmu.",
|
||||
"you_cannot_see_team_members": "V soukromém týmu nelze vidět všechny členy týmu.",
|
||||
"allow_booker_to_select_duration": "Umožnit rezervující osobě vybrat dobu trvání",
|
||||
"impersonate_user_tip": "Všechna použití této funkce jsou kontrolována.",
|
||||
@@ -1327,7 +1327,7 @@
|
||||
"download_responses_description": "Stáhněte si všechny odpovědi do formuláře ve formátu CSV.",
|
||||
"download": "Stáhnout",
|
||||
"download_recording": "Stáhnout nahrávku",
|
||||
"recording_from_your_recent_call": "Nahrávka z vašeho nedávného hovoru v {{appName}} je připravena ke stažení.",
|
||||
"recording_from_your_recent_call": "Nahrávka z vašeho nedávného hovoru v {{appName}} je připravena ke stažení",
|
||||
"create_your_first_form": "Vytvořte svůj první formulář",
|
||||
"create_your_first_form_description": "Pomocí adresných formulářů můžete klást kvalifikační otázky a adresovat je správné osobě nebo typu události.",
|
||||
"create_your_first_webhook": "Vytvořte svůj první webhook",
|
||||
@@ -1659,10 +1659,10 @@
|
||||
"delete_sso_configuration_confirmation_description": "Opravdu chcete odstranit konfiguraci {{connectionType}}? Členové vašeho týmu, kteří používají přihlášení {{connectionType}}, ztratí přístup k webu Cal.com.",
|
||||
"organizer_timezone": "Časové pásmo organizátora",
|
||||
"email_user_cta": "Zobrazit pozvánku",
|
||||
"email_no_user_invite_heading": "Byli jste pozváni, abyste se připojili k subjektu {{{entity}} v aplikaci {{appName}}",
|
||||
"email_no_user_invite_heading": "Byli jste pozváni, abyste se připojili k subjektu {{entity}} v aplikaci {{appName}}",
|
||||
"email_no_user_invite_subheading": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k jeho týmu v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu týmu umožňuje plánovat schůzky bez e-mailového ping-pongu.",
|
||||
"email_user_invite_subheading": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k týmu {{teamName}} subjektu ({{entity}}) v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu subjektu ({{entity}}) umožňuje plánovat schůzky bez e-mailového pingpongu.",
|
||||
"email_no_user_invite_steps_intro": "Provedeme vás několika krátkými kroky a za malou chvíli si budete se svým subjektem ({{entity}}) užívat plánování bez stresu.",
|
||||
"email_user_invite_subheading": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k týmu {{teamName}} subjektu {{entity}} v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu subjektu {{entity}} umožňuje plánovat schůzky bez e-mailového pingpongu.",
|
||||
"email_no_user_invite_steps_intro": "Provedeme vás několika krátkými kroky a za malou chvíli si budete se svým subjektem {{entity}} užívat plánování bez stresu.",
|
||||
"email_no_user_step_one": "Vyberte si uživatelské jméno",
|
||||
"email_no_user_step_two": "Připojte svůj účet kalendáře",
|
||||
"email_no_user_step_three": "Nastavte si dostupnost",
|
||||
@@ -1740,7 +1740,7 @@
|
||||
"no_responses_yet": "Zatím žádné odpovědi",
|
||||
"no_routes_defined": "Není definováno žádné směrování",
|
||||
"this_will_be_the_placeholder": "Toto bude zástupný znak",
|
||||
"error_booking_event": "Při rezervaci události došlo k chybě, obnovte prosím stránku a zkuste to znovu.",
|
||||
"error_booking_event": "Při rezervaci události došlo k chybě, obnovte prosím stránku a zkuste to znovu",
|
||||
"timeslot_missing_title": "Nebyl vybrán žádný časový slot",
|
||||
"timeslot_missing_description": "Vyberte časový slot, aby mohla být událost zaregistrována.",
|
||||
"timeslot_missing_cta": "Vybrat časový slot",
|
||||
@@ -1802,7 +1802,7 @@
|
||||
"least_booked_members": "Nejméně rezervovaných členů",
|
||||
"events_created": "Události vytvořeny",
|
||||
"events_completed": "Události dokončeny",
|
||||
"events_cancelled": "Události zrušeny",
|
||||
"events_cancelled": "Zrušené události",
|
||||
"events_rescheduled": "Události přesunuty",
|
||||
"from_last_period": "z posledního období",
|
||||
"from_to_date_period": "Od: {{startDate}} do: {{endDate}}",
|
||||
@@ -1838,9 +1838,9 @@
|
||||
"confirm_your_details": "Potvrďte své údaje",
|
||||
"copy_invite_link": "Zkopírovat odkaz pozvánky",
|
||||
"edit_invite_link": "Upravit nastavení odkazu",
|
||||
"invite_link_copied": "Odkaz pozvánky zkopírován",
|
||||
"invite_link_deleted": "Odkaz pozvánky odstraněn",
|
||||
"invite_link_updated": "Nastavení odkazu pozvánky uloženo",
|
||||
"invite_link_copied": "Odkaz pozvánky byl zkopírován",
|
||||
"invite_link_deleted": "Odkaz pozvánky byl odstraněn",
|
||||
"invite_link_updated": "Nastavení odkazu pozvánky bylo uloženo",
|
||||
"link_expires_after": "Platnost odkazů vyprší za...",
|
||||
"one_day": "1 den",
|
||||
"seven_days": "7 dní",
|
||||
@@ -1856,7 +1856,7 @@
|
||||
"email_invite_team_bulk": "Počet pozvaných uživatelů: {{userCount}}",
|
||||
"error_collecting_card": "Chyba při výběru karty",
|
||||
"image_size_limit_exceed": "Velikost nahraného obrázku by neměla překročit limit 5 MB",
|
||||
"unauthorized_workflow_error_message": "{{errorCode}}: Nejste oprávněni zapnout nebo vypnout tento pracovní postup.",
|
||||
"unauthorized_workflow_error_message": "{{errorCode}}: Nejste oprávněni zapnout nebo vypnout tento pracovní postup",
|
||||
"inline_embed": "Vložit na řádek",
|
||||
"load_inline_content": "Načte váš typ události přímo do řádků s ostatním obsahem webu.",
|
||||
"floating_pop_up_button": "Plovoucí překryvné tlačítko",
|
||||
@@ -1895,10 +1895,10 @@
|
||||
"about_your_organization": "Informace o vaší organizaci",
|
||||
"about_your_organization_description": "Organizace jsou sdílená prostředí, ve kterých můžete vytvořit několik týmů se sdílenými členy, typy událostí, aplikacemi, pracovními postupy a mnoha dalšími věcmi.",
|
||||
"create_your_teams": "Vytvoření týmů",
|
||||
"create_your_teams_description": "Přidejte do své organizace členy a pusťte se do společného plánování.",
|
||||
"create_your_teams_description": "Přidejte do své organizace členy a pusťte se do společného plánování",
|
||||
"invite_organization_admins": "Pozvěte správce své organizace",
|
||||
"invite_organization_admins_description": "Tito správci budou mít přístup ke všem týmům ve vaší organizaci. Správce a členy týmů můžete přidat později.",
|
||||
"set_a_password": "Nastavte heslo",
|
||||
"set_a_password": "Nastavte si heslo",
|
||||
"set_a_password_description": "Tím se vytvoří nový uživatelský účet s e-mailem vaší organizace a tímto heslem.",
|
||||
"organization_logo": "Logo organizace",
|
||||
"organization_about_description": "Několik vět o vaší organizaci. Objeví se na veřejné stránce profilu vaší organizace.",
|
||||
@@ -1923,7 +1923,7 @@
|
||||
"my_settings": "Moje nastavení",
|
||||
"crm": "CRM",
|
||||
"messaging": "Zasílání zpráv",
|
||||
"sender_id_info": "Jméno nebo číslo zobrazené jako odesílatel SMS (některé země nepovolují alfanumerické ID odesílatele).",
|
||||
"sender_id_info": "Jméno nebo číslo zobrazené jako odesílatel SMS (některé země nepovolují alfanumerické ID odesílatele)",
|
||||
"org_admins_can_create_new_teams": "Nové týmy může vytvářet pouze správce vaší organizace",
|
||||
"google_new_spam_policy": "Nové zásady společnosti Google týkající se nevyžádané pošty by mohly zabránit tomu, abyste dostávali e-mailová oznámení a oznámení kalendáře o této rezervaci.",
|
||||
"resolve": "Vyřešit",
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
"reset_password_subject": "{{appName}}: Anleitung zum Zurücksetzen des Passworts",
|
||||
"verify_email_subject": "{{appName}}: Bestätigen Sie Ihr Konto",
|
||||
"check_your_email": "Schauen Sie in Ihrem E-Mail-Postfach nach",
|
||||
"verify_email_page_body": "Wir haben eine E-Mail an {{email}} gesendet. Es ist wichtig, Ihre E-Mail-Adresse zu bestätigen, um die beste Zustellbarkeit für E-Mails und Kalender von {{appName}} zu gewährleisten.",
|
||||
"verify_email_banner_body": "Bestätigen Sie Ihre E-Mail-Adresse, um die beste Zustellbarkeit von E-Mails und Kalenderdaten zu gewährleisten",
|
||||
"verify_email_page_body": "Wir haben eine E-Mail an {{email}} gesendet. Es ist wichtig, Ihre E-Mail-Adresse zu bestätigen, um die Zustellbarkeit für E-Mails und Kalenderbenachrichtigungen von {{appName}} zu gewährleisten.",
|
||||
"verify_email_banner_body": "Bestätigen Sie Ihre E-Mail-Adresse, um die Zustellbarkeit von E-Mails und Kalenderbenachrichtigungen zu gewährleisten",
|
||||
"verify_email_email_header": "Bestätigen Sie Ihre E-Mail-Adresse",
|
||||
"verify_email_email_button": "E-Mail bestätigen",
|
||||
"copy_somewhere_safe": "Speichern Sie diesen API-Schlüssel an einem sicheren Ort. Sie können ihn nicht erneut ansehen.",
|
||||
@@ -137,11 +137,11 @@
|
||||
"upgrade_banner_action": "Hier aufrüsten",
|
||||
"team_upgraded_successfully": "Das Update für Ihr Team war erfolgreich!",
|
||||
"org_upgrade_banner_description": "Vielen Dank, dass Sie unseren neuen Organisationsplan getestet haben. Wir haben festgestellt, dass Ihre Organisation „{{teamName}}“ ein Upgrade benötigt.",
|
||||
"org_upgraded_successfully": "Das Update für Ihre Organisation war erfolgreich!",
|
||||
"org_upgraded_successfully": "Das Upgrade für Ihre Organization war erfolgreich!",
|
||||
"use_link_to_reset_password": "Benutzen Sie den Link, um Ihr Passwort zurückzusetzen",
|
||||
"hey_there": "Hallo,",
|
||||
"forgot_your_password_calcom": "Passwort vergessen? - {{appName}}",
|
||||
"delete_webhook_confirmation_message": "Sind Sie sicher, dass Sie diesen Webhook löschen möchten? Sie werden keine {{appName}} Meetingdaten mehr unter einer bestimmten URL, in Echtzeit erhalten, wenn ein Ereignis geplant oder abgesagt wird.",
|
||||
"delete_webhook_confirmation_message": "Sind Sie sicher, dass Sie diesen Webhook löschen möchten? Sie werden keine {{appName}}-Meetingdaten mehr unter einer bestimmten URL, in Echtzeit erhalten, wenn ein Ereignis geplant oder abgesagt wird.",
|
||||
"confirm_delete_webhook": "Ja, Webhook löschen",
|
||||
"edit_webhook": "Webhook bearbeiten",
|
||||
"delete_webhook": "Webhook löschen",
|
||||
@@ -260,7 +260,7 @@
|
||||
"organization_general_description": "Einstellungen für die Sprache und Zeitzone Ihres Teams verwalten",
|
||||
"few_sentences_about_yourself": "Schreiben Sie eine kurze Beschreibung, welche auf Ihrer persönlichen URL-Seite erscheinen wird.",
|
||||
"nearly_there": "Fast fertig",
|
||||
"nearly_there_instructions": "Zu guter Letzt: Ein Foto und eine Kurzbeschreibung zu Ihnen helfen, öfter gebucht zu werden, da Kunden dann besser wissen, wen sie buchen.",
|
||||
"nearly_there_instructions": "Zu guter Letzt: Ein Foto und eine Kurzbeschreibung zu Ihnen helfen, öfter gebucht zu werden, da Kunden dann genauer wissen, bei wem sie einen Termin buchen.",
|
||||
"set_availability_instructions": "Definieren Sie Zeiträume, wenn Sie wiederkehrend verfügbar sind. Sie können später weitere Zeiträume erstellen und verschiedenen Kalendern zuordnen.",
|
||||
"set_availability": "Verfügbarkeit festlegen",
|
||||
"continue_without_calendar": "Ohne Kalender fortfahren",
|
||||
@@ -318,11 +318,11 @@
|
||||
"password_has_been_reset_login": "Ihr Passwort wurde zurückgesetzt. Sie können sich jetzt mit Ihrem neu erstellten Passwort anmelden.",
|
||||
"layout": "Layout",
|
||||
"bookerlayout_default_title": "Standardansicht",
|
||||
"bookerlayout_description": "Sie können mehrere auswählen und Ihre Bucher können die Ansicht wechseln.",
|
||||
"bookerlayout_description": "Sie können mehrere auswählen und Ihre Bucher können anschließend die Ansicht wechseln.",
|
||||
"bookerlayout_user_settings_title": "Buchungslayout",
|
||||
"bookerlayout_user_settings_description": "Sie können mehrere auswählen und Bucher können die Ansicht wechseln. Dies kann pro Termin überschrieben werden.",
|
||||
"bookerlayout_user_settings_description": "Sie können mehrere auswählen und Bucher können anschließend die Ansicht wechseln. Dies kann pro Termin überschrieben werden.",
|
||||
"bookerlayout_month_view": "Monat",
|
||||
"bookerlayout_week_view": "Woche",
|
||||
"bookerlayout_week_view": "Wöchentlich",
|
||||
"bookerlayout_column_view": "Spalte",
|
||||
"bookerlayout_error_min_one_enabled": "Es muss mindestens ein Layout aktiviert sein.",
|
||||
"bookerlayout_error_default_not_enabled": "Das Layout, das Sie als Standardansicht ausgewählt haben, ist nicht Teil der aktivierten Layouts.",
|
||||
@@ -404,6 +404,8 @@
|
||||
"allow_dynamic_booking_tooltip": "Gruppenbuchungslinks, die dynamisch durch Hinzufügen mehrerer Benutzernamen mit einem '+' erstellt werden können. Beispiel: '{{appName}}/bailey+peer'",
|
||||
"allow_dynamic_booking": "Ermöglichen Sie es den Teilnehmenden, Sie über dynamische Gruppenbuchungen zu buchen",
|
||||
"dynamic_booking": "Dynamische Gruppenlinks",
|
||||
"allow_seo_indexing": "Suchmaschinen erlauben, auf Ihre öffentlichen Inhalte zuzugreifen",
|
||||
"seo_indexing": "Suchmaschinen-Indexierung erlauben",
|
||||
"email": "E-Mail",
|
||||
"email_placeholder": "max@example.com",
|
||||
"full_name": "Vollständiger Name",
|
||||
@@ -426,8 +428,8 @@
|
||||
"team_webhooks": "Team-Webhooks",
|
||||
"create_new_webhook_to_account": "Erstellen Sie einen neuen Webhook in Ihrem Account",
|
||||
"new_webhook": "Neuer Webhook",
|
||||
"receive_cal_meeting_data": "Erhalten Sie {{appName}} Termindaten unter einer bestimmten URL in Echtzeit, wenn ein Termin geplant oder abgesagt wird.",
|
||||
"receive_cal_event_meeting_data": "Erhalten Sie {{appName}} Termindaten unter einer bestimmten URL in Echtzeit, wenn dieser Termin geplant oder abgesagt wird.",
|
||||
"receive_cal_meeting_data": "Erhalten Sie in Echtzeit {{appName}}-Termindaten unter einer bestimmten URL, wenn ein Termin geplant oder abgesagt wird.",
|
||||
"receive_cal_event_meeting_data": "Erhalten Sie in Echtzeit {{appName}}-Termindaten unter einer bestimmten URL, wenn dieser Termin geplant oder abgesagt wird.",
|
||||
"responsive_fullscreen_iframe": "Responsives Vollbild-iFrame",
|
||||
"loading": "Wird geladen...",
|
||||
"deleting": "Lösche...",
|
||||
@@ -565,7 +567,7 @@
|
||||
"team_description": "Ein paar Sätze über Ihr Team auf der öffentlichen Teamseite.",
|
||||
"org_description": "Ein paar Sätze zu Ihrer Organisation. Dies wird auf der URL-Seite Ihrer Organisation erscheinen.",
|
||||
"members": "Mitglieder",
|
||||
"organization_members": "Organisationsmitglieder",
|
||||
"organization_members": "Mitglieder der Organization",
|
||||
"member": "Mitglied",
|
||||
"number_member_one": "{{count}} Mitglied",
|
||||
"number_member_other": "{{count}} Mitglieder",
|
||||
@@ -600,7 +602,7 @@
|
||||
"confirm_all": "Alle bestätigen",
|
||||
"disband_team": "Team auflösen",
|
||||
"disband_team_confirmation_message": "Bist du sicher, dass du dieses Team auflösen möchtest? Jeder der diesen Team-Link erhalten hat, kann Sie nicht mehr buchen.",
|
||||
"disband_org": "Organisation auflösen",
|
||||
"disband_org": "Organization auflösen",
|
||||
"disband_org_confirmation_message": "Sind Sie sicher, dass Sie diese Organisation auflösen möchten? Alle Teams und Mitglieder werden gelöscht.",
|
||||
"remove_member_confirmation_message": "Sind Sie sicher, dass Sie dieses Mitglied aus dem Team entfernen möchten?",
|
||||
"confirm_disband_team": "Ja, Team auflösen",
|
||||
@@ -713,7 +715,7 @@
|
||||
"create_team_to_get_started": "Erstellen Sie ein Team um loszulegen",
|
||||
"teams": "Teams",
|
||||
"team": "Team",
|
||||
"organization": "Organisation",
|
||||
"organization": "Organization",
|
||||
"team_billing": "Abrechnung der Teams",
|
||||
"team_billing_description": "Verwalte die Abrechungen deines Teams",
|
||||
"upgrade_to_flexible_pro_title": "Wir haben die Abrechnung für Teams geändert",
|
||||
@@ -924,7 +926,7 @@
|
||||
"create_events_on": "Erstelle Termine in:",
|
||||
"enterprise_license": "Das ist eine Enterprise-Funktion",
|
||||
"enterprise_license_description": "Um diese Funktion zu aktivieren, holen Sie sich einen Deployment-Schlüssel von der {{consoleUrl}}-Konsole und fügen Sie ihn als CALCOM_LICENSE_KEY zu Ihrer .env hinzu. Wenn Ihr Team bereits eine Lizenz hat, wenden Sie sich bitte an {{supportMail}} für Hilfe.",
|
||||
"enterprise_license_development": "Sie können diese Funktion im Entwicklungsmodus testen. Zur Nutzung im Produktionsmodus, besuchen Sie bitte <2>/auth/setup</2>, um einen Lizenzschlüssel einzugeben.",
|
||||
"enterprise_license_development": "Sie können diese Funktion im Entwicklungsmodus testen. Zur Nutzung im regulären Arbeitsumfeld, besuchen Sie bitte <2>/auth/setup</2>, um einen Lizenzschlüssel einzugeben.",
|
||||
"missing_license": "Lizenz fehlt",
|
||||
"signup_requires": "Kommerzielle Lizenz erforderlich",
|
||||
"signup_requires_description": "{{companyName}} bietet zur Zeit keine kostenlose Open-Source-Version der Anmeldeseite an. Um vollen Zugriff auf die Registrierungskomponenten zu erhalten, müssen Sie eine kommerzielle Lizenz erwerben. Für den persönlichen Gebrauch empfehlen wir die Prisma Data Platform oder jede andere Postgres-Schnittstelle zur Erstellung von Accounts.",
|
||||
@@ -1018,7 +1020,7 @@
|
||||
"user_impersonation_description": "Erlaubt es unserem Support-Team, sich vorübergehend mit Ihrem Konto anzumelden, um jegliche Probleme, die Sie gemeldet haben, schneller zu lösen.",
|
||||
"team_impersonation_description": "Ermöglicht den Team-Administratoren, sich vorübergehend als Sie einzuloggen.",
|
||||
"make_team_private": "Team privat stellen",
|
||||
"make_team_private_description": "Deine Teammitglieder können keine anderen Teammitglieder sehen, wenn dies aktiviert ist.",
|
||||
"make_team_private_description": "Wenn diese Einstellung aktiv ist, können deine Teammitglieder keine anderen Teammitglieder sehen.",
|
||||
"you_cannot_see_team_members": "Sie können nicht alle Teammitglieder eines privaten Teams sehen.",
|
||||
"allow_booker_to_select_duration": "Bucher erlauben, die Länge zu wählen",
|
||||
"impersonate_user_tip": "Alle Verwendungen dieser Funktion werden geprüft.",
|
||||
@@ -1351,9 +1353,9 @@
|
||||
"event_one": "Ereignis",
|
||||
"event_other": "Ereignisse",
|
||||
"profile_team_description": "Einstellungen für Ihr Teamprofil verwalten",
|
||||
"profile_org_description": "Einstellungen für Ihr Organisationsprofil verwalten",
|
||||
"profile_org_description": "Einstellungen für Ihr Organization-Profil verwalten",
|
||||
"members_team_description": "Benutzer in der Gruppe",
|
||||
"organization_description": "Administratoren und Mitglieder in Ihrer Organisation verwalten",
|
||||
"organization_description": "Administratoren und Mitglieder in Ihrer Organization verwalten",
|
||||
"team_url": "Team-URL",
|
||||
"team_members": "Teammitglieder",
|
||||
"more": "Mehr",
|
||||
@@ -1499,7 +1501,7 @@
|
||||
"yearly": "Jährlich",
|
||||
"checkout": "Kasse",
|
||||
"your_team_disbanded_successfully": "Ihr Team wurde erfolgreich aufgelöst",
|
||||
"your_org_disbanded_successfully": "Ihre Organisation wurde erfolgreich aufgelöst",
|
||||
"your_org_disbanded_successfully": "Ihre Organization wurde erfolgreich aufgelöst",
|
||||
"error_creating_team": "Fehler beim Erstellen des Teams",
|
||||
"you": "Sie",
|
||||
"resend_email": "E-Mail erneut senden",
|
||||
@@ -1511,7 +1513,7 @@
|
||||
"url_taken": "Diese URL ist bereits vergeben",
|
||||
"problem_registering_domain": "Es gab ein Problem bei der Registrierung der Subdomain, bitte versuchen Sie es erneut oder kontaktieren Sie einen Administrator",
|
||||
"team_publish": "Team veröffentlichen",
|
||||
"number_text_notifications": "Telefonnummer (Text-Benachrichtigungen)",
|
||||
"number_text_notifications": "Telefonnummer (Textbenachrichtigungen)",
|
||||
"attendee_email_variable": "Teilnehmer E-Mail",
|
||||
"attendee_email_info": "Die E-Mail-Adresse der buchenden Person",
|
||||
"kbar_search_placeholder": "Geben Sie einen Befehl ein oder suchen Sie ...",
|
||||
@@ -1534,8 +1536,8 @@
|
||||
"find_the_best_person": "Finden Sie die beste verfügbare Person und rotieren Sie durch Ihr Team.",
|
||||
"fixed_round_robin": "Festgelegter Round Robin",
|
||||
"add_one_fixed_attendee": "Fügen Sie einen festen Teilnehmer und einen Round Robin mit einer Reihe von Teilnehmern hinzu.",
|
||||
"calcom_is_better_with_team": "{{appName}} ist mit Teams besser",
|
||||
"the_calcom_team": "Das {{companyName}} Team",
|
||||
"calcom_is_better_with_team": "{{appName}} ist mit Teams noch besser",
|
||||
"the_calcom_team": "Das {{companyName}}-Team",
|
||||
"add_your_team_members": "Fügen Sie Ihre Teammitglieder zu Ihren Termintypen hinzu. Nutzen Sie kollektive Planung, um alle einzubinden oder finden Sie die geeignetste Person mit der Round Robin Planung.",
|
||||
"booking_limit_reached": "Das Buchungslimit für diesen Termintyp wurde erreicht",
|
||||
"duration_limit_reached": "Das Zeitlimit für diesen Termintyp wurde erreicht",
|
||||
@@ -1688,7 +1690,7 @@
|
||||
"scheduler": "{Scheduler}",
|
||||
"no_workflows": "Keine Workflows",
|
||||
"change_filter": "Ändern Sie den Filter, um Ihre eigenen und Ihre Team-Workflows anzuzeigen.",
|
||||
"change_filter_common": "Ändern Sie Ihre Filteroptionen um die Ergebnisse zu sehen.",
|
||||
"change_filter_common": "Passen Sie Ihre Filteroptionen an, um die Ergebnisse zu sehen.",
|
||||
"no_results_for_filter": "Keine Ergebnisse für diesen Filter",
|
||||
"recommended_next_steps": "Empfohlene nächste Schritte",
|
||||
"create_a_managed_event": "Einen verwalteten Ereignistyp erstellen",
|
||||
@@ -1700,7 +1702,7 @@
|
||||
"no_longer_attending": "Sie nehmen an diesem Termin nicht mehr teil",
|
||||
"attendee_no_longer_attending_subject": "Ein Teilnehmer nimmt nicht mehr an {{title}} am {{date}} teil",
|
||||
"attendee_no_longer_attending": "Ein Teilnehmer nimmt nicht mehr an Ihrem Termin teil",
|
||||
"attendee_no_longer_attending_subtitle": "{{name}} hat abgesagt. Das bedeutet, dass ein Platz für dieses Zeitfenster geöffnet wurde",
|
||||
"attendee_no_longer_attending_subtitle": "{{name}} hat abgesagt. Dies bedeutet, dass ein Platz in diesem Zeitfenster frei wurde",
|
||||
"create_event_on": "Erstelle Termin am",
|
||||
"create_routing_form_on": "Weiterleitungsformular erstellen am",
|
||||
"default_app_link_title": "Standard-App-Link festlegen",
|
||||
@@ -1753,7 +1755,7 @@
|
||||
"spot_popular_event_types": "Machen Sie beliebte Ereignistypen ausfindig",
|
||||
"spot_popular_event_types_description": "Sehen Sie, welche Ihrer Ereignistypen die meisten Klicks und Buchungen erhalten",
|
||||
"no_responses_yet": "Noch keine Antworten",
|
||||
"no_routes_defined": "Keine Routen definiert",
|
||||
"no_routes_defined": "Keine Weiterleitungsrouten definiert",
|
||||
"this_will_be_the_placeholder": "Dies wird der Platzhalter sein",
|
||||
"error_booking_event": "Bei der Buchung dieses Termins ist ein Fehler aufgetreten, bitte aktualisieren Sie die Seite und versuchen Sie es erneut",
|
||||
"timeslot_missing_title": "Kein Zeitfenster ausgewählt",
|
||||
@@ -1896,68 +1898,69 @@
|
||||
"first_event_type_webhook_description": "Erstellen Sie Ihren ersten Webhook für diesen Termintypen",
|
||||
"install_app_on": "App installieren auf",
|
||||
"create_for": "Erstellen für",
|
||||
"setup_organization": "Eine Organisation einrichten",
|
||||
"organization_banner_description": "Schaffen Sie eine Umgebung, in der Teams gemeinsame Apps, Workflows und Termintypen mit Round Robin und kollektiver Terminplanung erstellen können.",
|
||||
"organization_banner_title": "Verwalten Sie Organisationen mit mehreren Teams",
|
||||
"set_up_your_organization": "Ihre Organisation einrichten",
|
||||
"organizations_description": "Organisationen sind geteilte Umgebungen, in denen Teams gemeinsame Termintypen, Apps, Workflows und mehr erstellen können.",
|
||||
"must_enter_organization_name": "Es muss ein Organisationsname eingegeben werden",
|
||||
"must_enter_organization_admin_email": "Sie müssen Ihre Organisations-Mailadresse eingeben",
|
||||
"admin_email": "Ihre Organisations-E-Mail-Adresse",
|
||||
"currency": "Währung",
|
||||
"setup_organization": "Eine Organization einrichten",
|
||||
"organization_banner_description": "Schaffen Sie Umgebungen, in der Teams gemeinsame Apps, Workflows und Termintypen mit Round Robin und kollektiver Terminplanung erstellen können.",
|
||||
"organization_banner_title": "Verwalten Sie Organizations mit mehreren Teams",
|
||||
"set_up_your_organization": "Ihre Organization einrichten",
|
||||
"organizations_description": "Organizations sind geteilte Umgebungen, in denen Teams gemeinsame Termintypen, Apps, Workflows und mehr erstellen können.",
|
||||
"must_enter_organization_name": "Es muss ein Name für die Organization eingegeben werden",
|
||||
"must_enter_organization_admin_email": "Sie müssen Ihre Organization-Mailadresse eingeben\n",
|
||||
"admin_email": "Ihre Organization-E-Mail-Adresse",
|
||||
"admin_username": "Benutzername des Administrators",
|
||||
"organization_name": "Name der Organisation",
|
||||
"organization_url": "URL der Organisation",
|
||||
"organization_verify_header": "Bestätigen Sie Ihre Organisations-Mailadresse",
|
||||
"organization_verify_email_body": "Bitte verwenden Sie den folgenden Code, um Ihre E-Mail-Adresse zu bestätigen, um die Einrichtung Ihrer Organisation fortzusetzen.",
|
||||
"organization_name": "Name der Organization",
|
||||
"organization_url": "URL der Organization ",
|
||||
"organization_verify_header": "Bestätigen Sie Ihre Organization-Mailadresse",
|
||||
"organization_verify_email_body": "Bitte verwenden Sie den folgenden Code, um Ihre E-Mail-Adresse zu bestätigen, um die Einrichtung Ihrer Organization fortzusetzen.",
|
||||
"additional_url_parameters": "Zusätzliche URL-Parameter",
|
||||
"about_your_organization": "Über Ihre Organisation",
|
||||
"about_your_organization_description": "Organisationen sind geteilte Umgebungen, in denen Sie mehrere Teams mit gemeinsamen Mitgliedern, Termintypen, Apps, Workflows und mehr erstellen können.",
|
||||
"about_your_organization": "Über Ihre Organization",
|
||||
"about_your_organization_description": "Organizations sind geteilte Umgebungen, in denen Sie mehrere Teams mit gemeinsamen Mitgliedern, Termintypen, Apps, Workflows und mehr erstellen können.\n",
|
||||
"create_your_teams": "Erstellen Sie Ihre Teams",
|
||||
"create_your_teams_description": "Fangen Sie an, gemeinsam zu planen, indem Sie Ihre Teammitglieder zu Ihrer Organisation hinzufügen",
|
||||
"invite_organization_admins": "Laden Sie die Administratoren Ihrer Organisation ein",
|
||||
"invite_organization_admins_description": "Diese Administratoren haben Zugriff auf alle Teams in Ihrer Organisation. Sie können Team-Administratoren und -mitglieder später hinzufügen.",
|
||||
"create_your_teams_description": "Fangen Sie an, gemeinsam zu planen, indem Sie Ihre Teammitglieder zu Ihrer Organization hinzufügen",
|
||||
"invite_organization_admins": "Laden Sie die Administratoren Ihrer Organization ein",
|
||||
"invite_organization_admins_description": "Diese Administratoren haben Zugriff auf alle Teams in Ihrer Organization. Sie können Team-Administratoren und -mitglieder später hinzufügen.",
|
||||
"set_a_password": "Legen Sie ein Passwort fest",
|
||||
"set_a_password_description": "Dies wird ein neues Benutzerkonto mit Ihrer Organisations-Mailadresse und diesem Passwort erstellen.",
|
||||
"organization_logo": "Logo der Organisation",
|
||||
"organization_about_description": "Ein paar Sätze zu Ihrer Organisation. Dies wird auf der öffentlichen Profilseite Ihrer Organisation erscheinen.",
|
||||
"set_a_password_description": "Dies wird ein neues Benutzerkonto mit Ihrer Organization-Mailadresse und diesem Passwort erstellen.",
|
||||
"organization_logo": "Logo der Organization",
|
||||
"organization_about_description": "Ein paar Sätze zu Ihrer Organization. Dies wird auf der öffentlichen Profilseite Ihrer Organization erscheinen.",
|
||||
"ill_do_this_later": "Ich werde das später tun",
|
||||
"verify_your_email": "Bestätigen Sie Ihre E-Mail",
|
||||
"verify_your_email": "Bestätigen Sie Ihre E-Mail-Adresse",
|
||||
"enter_digit_code": "Geben Sie den 6-stelligen Code ein, den wir an {{email}} gesendet haben",
|
||||
"verify_email_organization": "Bestätigen Sie Ihre E-Mail, um eine Organisation zu erstellen",
|
||||
"verify_email_organization": "Bestätigen Sie Ihre E-Mail-Adresse, um eine Organization zu erstellen",
|
||||
"code_provided_invalid": "Der eingegebene Code ist ungültig, versuchen Sie es erneut",
|
||||
"email_already_used": "Die E-Mail-Adresse wird bereits verwendet",
|
||||
"organization_admin_invited_heading": "Sie wurden eingeladen, ein Mitglied von {{orgName}} zu werden",
|
||||
"organization_admin_invited_body": "Schließen Sie sich Ihrem Team bei {{orgName}} an und konzentrieren Sie sich auf Ihre Meetings, nicht auf das Planen von Meetings!",
|
||||
"duplicated_slugs_warning": "Die folgenden Teams konnten aufgrund von doppelten Slugs nicht erstellt werden: {{slugs}}",
|
||||
"team_names_empty": "Teamnamen dürfen nicht leer sein",
|
||||
"team_names_repeated": "Teamnamen dürfen nicht wiederholt vorkommen",
|
||||
"user_belongs_organization": "Der Benutzer gehört einer Organisation an",
|
||||
"org_no_teams_yet": "Diese Organisation hat noch keine Teams",
|
||||
"team_names_repeated": "Teamnamen dürfen nicht bereits verwendet werden",
|
||||
"user_belongs_organization": "Der Benutzer gehört einer Organization an",
|
||||
"org_no_teams_yet": "Diese Organization hat noch keine Teams",
|
||||
"org_no_teams_yet_description": "Falls Sie ein Administrator sind, sollten Sie unbedingt Teams erstellen, welche hier angezeigt werden können.",
|
||||
"set_up": "Einrichten",
|
||||
"set_up_your_profile": "Richten Sie Ihr Profil ein",
|
||||
"set_up_your_profile_description": "Lassen Sie Leute wissen, wer Sie innerhalb von {{orgName}} sind und wann sie mit Ihrem öffentlichen Link arbeiten.",
|
||||
"set_up_your_profile_description": "Lassen Sie Leute beim Klicken auf Ihren öffentlichen Link wissen, welche Funktion Sie innerhalb von {{orgName}} haben.",
|
||||
"my_profile": "Mein Profil",
|
||||
"my_settings": "Meine Einstellungen",
|
||||
"crm": "CRM",
|
||||
"messaging": "Nachrichten",
|
||||
"sender_id_info": "Name oder Nummer, welcher bzw. welche als Absender einer SMS angezeigt wird (einige Länder erlauben keine alphanumerischen Absender-IDs)",
|
||||
"org_admins_can_create_new_teams": "Nur der Administrator Ihrer Organisation kann neue Teams erstellen",
|
||||
"org_admins_can_create_new_teams": "Nur der Administrator Ihrer Organization kann neue Teams erstellen",
|
||||
"google_new_spam_policy": "Die neuen Spam-Richtlinien von Google könnten verhindern, dass Sie E-Mail- und Kalenderbenachrichtigungen zu dieser Buchung erhalten.",
|
||||
"resolve": "Beheben",
|
||||
"no_organization_slug": "Beim Erstellen von Teams für diese Organisation ist ein Fehler aufgetreten. URL Slug fehlt.",
|
||||
"org_name": "Name der Organisation",
|
||||
"org_url": "URL der Organisation",
|
||||
"copy_link_org": "Link zur Organisation kopieren",
|
||||
"404_the_org": "Die Organisation",
|
||||
"resolve": "Problem lösen",
|
||||
"no_organization_slug": "Beim Erstellen von Teams für diese Organization ist ein Fehler aufgetreten. URL-Slug fehlt.",
|
||||
"org_name": "Name der Organization",
|
||||
"org_url": "URL der Organization",
|
||||
"copy_link_org": "Link zur Organization kopieren",
|
||||
"404_the_org": "Die Organization",
|
||||
"404_the_team": "Das Team",
|
||||
"404_claim_entity_org": "Beanspruchen Sie Ihre Subdomain für Ihre Organisation",
|
||||
"404_claim_entity_org": "Beanspruchen Sie Ihre Subdomain für Ihre Organization",
|
||||
"404_claim_entity_team": "Dieses Team beanspruchen und gemeinsam mit der Verwaltung von Verfügbarkeitsplänen beginnen",
|
||||
"insights_all_org_filter": "Alle Apps",
|
||||
"insights_team_filter": "Team: {{teamName}}",
|
||||
"insights_user_filter": "Benutzer: {{userName}}",
|
||||
"insights_subtitle": "Sehen Sie sich Buchungs-Insights über Ihre Termine an",
|
||||
"custom_plan": "Benutzerdefinierter Plan",
|
||||
"insights_subtitle": "Sehen Sie sich Buchungs-Insights zu Ihren Termine an",
|
||||
"custom_plan": "Maßgeschneiderter Tarif",
|
||||
"email_embed": "E-Mail Einbettung",
|
||||
"add_times_to_your_email": "Zeiten zur E-Mail hinzufügen",
|
||||
"select_time": "Zeit auswählen",
|
||||
@@ -1971,10 +1974,13 @@
|
||||
"org_team_names_example_5": "z.B. Data Analytics Team",
|
||||
"org_max_team_warnings": "Weitere Teams können Sie zu einem späteren Zeitpunkt hinzufügen.",
|
||||
"what_is_this_meeting_about": "Worum geht es in diesem Termin?",
|
||||
"kyc_verification_information": "Aus Sicherheitsgründen müssen Sie Ihren {{teamOrAccount}} verifizieren, bevor Sie Textnachrichten an die Teilnehmer senden können. Bitte kontaktieren Sie uns unter <a>{{supportEmail}}</a> und geben Sie folgende Informationen an:",
|
||||
"org_admin_other_teams": "Weitere Teams",
|
||||
"no_other_teams_found": "Keine weiteren Teams gefunden",
|
||||
"no_other_teams_found_description": "Es gibt keine weiteren Teams in dieser Organisation.",
|
||||
"attendee_first_name_variable": "Vorname des Teilnehmers",
|
||||
"attendee_last_name_variable": "Nachname des Teilnehmers",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Zeichenketten über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
"my_availability": "Meine Verfügbarkeit",
|
||||
"team_availability": "Teamverfügbarkeit",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -407,6 +407,8 @@
|
||||
"allow_dynamic_booking_tooltip": "Group booking links that can be created dynamically by adding multiple usernames with a '+'. example: '{{appName}}/bailey+peer'",
|
||||
"allow_dynamic_booking": "Allow attendees to book you through dynamic group bookings",
|
||||
"dynamic_booking": "Dynamic group links",
|
||||
"allow_seo_indexing": "Allow search engines to access your public content",
|
||||
"seo_indexing": "Allow SEO Indexing",
|
||||
"email": "Email",
|
||||
"email_placeholder": "jdoe@example.com",
|
||||
"full_name": "Full name",
|
||||
@@ -560,6 +562,7 @@
|
||||
"leave": "Leave",
|
||||
"profile": "Profile",
|
||||
"my_team_url": "My team URL",
|
||||
"my_teams": "My teams",
|
||||
"team_name": "Team Name",
|
||||
"your_team_name": "Your team name",
|
||||
"team_updated_successfully": "Team updated successfully",
|
||||
@@ -806,7 +809,6 @@
|
||||
"add_new_event_type": "Add a new event type",
|
||||
"new_event_type_to_book_description": "Create a new event type for people to book times with.",
|
||||
"length": "Length",
|
||||
"people":"People",
|
||||
"minimum_booking_notice": "Minimum Notice",
|
||||
"offset_toggle": "Offset start times",
|
||||
"offset_toggle_description": "Offset timeslots shown to bookers by a specified number of minutes",
|
||||
@@ -870,6 +872,7 @@
|
||||
"account_managed_by_identity_provider_description": "To change your email, password, enable two-factor authentication and more, please visit your {{provider}} account settings.",
|
||||
"signin_with_google": "Sign in with Google",
|
||||
"signin_with_saml": "Sign in with SAML",
|
||||
"signin_with_saml_oidc": "Sign in with SAML/OIDC",
|
||||
"you_will_need_to_generate": "You will need to generate an access token from your old scheduling tool.",
|
||||
"import": "Import",
|
||||
"import_from": "Import from",
|
||||
@@ -927,7 +930,7 @@
|
||||
"verify_wallet": "Verify Wallet",
|
||||
"create_events_on": "Create events on",
|
||||
"enterprise_license": "This is an enterprise feature",
|
||||
"enterprise_license_description": "To enable this feature, have an administrator go to {{setupUrl}} to enter a license key. If a license key is already in place, please contact {{supportMail}} for help.",
|
||||
"enterprise_license_description": "To enable this feature, have an administrator go to <2>/auth/setup</2> to enter a license key. If a license key is already in place, please contact <5>{{SUPPORT_MAIL_ADDRESS}}</5> for help.",
|
||||
"enterprise_license_development": "You can test this feature on development mode. For production usage please have an administrator go to <2>/auth/setup</2> to enter a license key.",
|
||||
"missing_license": "Missing License",
|
||||
"signup_requires": "Commercial license required",
|
||||
@@ -1263,6 +1266,7 @@
|
||||
"error_updating_settings": "Error updating settings",
|
||||
"personal_cal_url": "My personal {{appName}} URL",
|
||||
"bio_hint": "A few sentences about yourself. this will appear on your personal url page.",
|
||||
"user_has_no_bio": "This user has not added a bio yet.",
|
||||
"delete_account_modal_title": "Delete Account",
|
||||
"confirm_delete_account_modal": "Are you sure you want to delete your {{appName}} account?",
|
||||
"delete_my_account": "Delete my account",
|
||||
@@ -1578,7 +1582,6 @@
|
||||
"apps_settings": "Apps settings",
|
||||
"fill_this_field": "Please fill in this field",
|
||||
"options": "Options",
|
||||
"no_options_available":"No options available",
|
||||
"enter_option": "Enter Option {{index}}",
|
||||
"add_an_option": "Add an option",
|
||||
"radio": "Radio",
|
||||
@@ -1980,6 +1983,11 @@
|
||||
"org_team_names_example_5": "e.g. Data Analytics Team",
|
||||
"org_max_team_warnings": "You will be able to add more teams later on.",
|
||||
"what_is_this_meeting_about": "What is this meeting about?",
|
||||
"add_to_team":"Add to team",
|
||||
"remove_users_from_org": "Remove users from organization",
|
||||
"remove_users_from_org_confirm":"Are you sure you want to remove {{userCount}} users from this organization?",
|
||||
"user_has_no_schedules":"This user has not setup any schedules yet",
|
||||
"user_isnt_in_any_teams":"This user is not in any teams",
|
||||
"requires_booker_email_verification": "Requires booker email verification",
|
||||
"description_requires_booker_email_verification": "To ensure booker's email verification before scheduling events",
|
||||
"requires_confirmation_mandatory": "Text messages can only be sent to attendees when event type requires confirmation.",
|
||||
@@ -1990,7 +1998,7 @@
|
||||
"kyc_verification": "KYC Verification",
|
||||
"organizations": "Organizations",
|
||||
"org_admin_other_teams": "Other teams",
|
||||
"org_admin_other_teams_description": "Here you can see teams inside your organization but that you are not part of. You can add yourself to them if needed.",
|
||||
"org_admin_other_teams_description": "Here you can see teams inside your organization that you are not part of. You can add yourself to them if needed.",
|
||||
"no_other_teams_found": "No other teams found",
|
||||
"no_other_teams_found_description": "There are no other teams in this organization.",
|
||||
"attendee_first_name_variable": "Attendee first name",
|
||||
@@ -1999,5 +2007,24 @@
|
||||
"attendee_last_name_info": "The person booking's last name",
|
||||
"me": "Me",
|
||||
"verify_team_tooltip": "Verify your team to enable sending messages to attendees",
|
||||
"member_removed": "Member removed",
|
||||
"my_availability": "My Availability",
|
||||
"team_availability": "Team Availability",
|
||||
"admin_org_notification_email_subject": "New organization created: pending action",
|
||||
"hi_admin": "Hi Administrator",
|
||||
"admin_org_notification_email_title": "An organization requires DNS setup",
|
||||
"admin_org_notification_email_body_part1": "An organization with slug \"{{orgSlug}}\" was created.<br /><br />Please be sure to configure your DNS registry to point the subdomain corresponding to the new organization to where the main app is running. Otherwise the organization will not work.<br /><br />Here are just the very basic options to configure a subdomain to point to their app so it loads the organization profile page.<br /><br />You can do it either with the A Record:",
|
||||
"admin_org_notification_email_body_part2": "Or the CNAME record:",
|
||||
"admin_org_notification_email_body_part3": "Once you configure the subdomain, please mark the DNS configuration as done in Organizations Admin Settings.",
|
||||
"admin_org_notification_email_cta": "Go to Organizations Admin Settings",
|
||||
"org_has_been_processed": "Org has been processed",
|
||||
"org_error_processing": "There has been an error processing this organization",
|
||||
"orgs_page_description": "A list of all organizations. Accepting an organization will allow all users with that email domain to sign up WITHOUT email verifciation.",
|
||||
"unverified": "Unverified",
|
||||
"dns_missing": "DNS Missing",
|
||||
"mark_dns_configured": "Mark as DNS configured",
|
||||
"value": "Value",
|
||||
"your_organization_updated_sucessfully": "Your organization updated successfully",
|
||||
"seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
"calcom_explained_new_user": "¡Termine de configurar su cuenta de {{appName}}! Solo le faltan unos pasos para resolver todos sus problemas de programación.",
|
||||
"have_any_questions": "¿Tienes preguntas? Estamos aquí para ayudar.",
|
||||
"reset_password_subject": "{{appName}}: Instrucciones para restablecer la contraseña",
|
||||
"verify_email_subject": "{{appName}}: verifique su cuenta",
|
||||
"verify_email_subject": "{{appName}}: Verifique su cuenta",
|
||||
"check_your_email": "Revise su correo electrónico",
|
||||
"verify_email_page_body": "Enviamos un correo electrónico a {{email}}. Es importante que verifique su dirección de correo electrónico para garantizar que {{appName}} le ofrezca el mejor servicio de entrega de correos y calendarios.",
|
||||
"verify_email_banner_body": "Verifique su dirección de correo electrónico para garantizar la mejor entrega de correo electrónico y calendario",
|
||||
"verify_email_email_header": "Verifique su dirección de correo electrónico",
|
||||
"verify_email_email_button": "Verificar correo electrónico",
|
||||
"verify_email_email_body": "Verifique su dirección de correo electrónico al hacer clic en el botón a continuación.",
|
||||
"verify_email_email_link_text": "Este es el enlace en caso de que no le guste hacer clic en los botones:",
|
||||
"verify_email_email_link_text": "Este es el enlace en caso de que no prefiera hacer clic en los botones:",
|
||||
"email_sent": "Correo electrónico enviado correctamente",
|
||||
"event_declined_subject": "Rechazado: {{title}} en {{date}}",
|
||||
"event_cancelled_subject": "Cancelado: {{title}} el {{date}}",
|
||||
@@ -130,7 +130,7 @@
|
||||
"upgrade_banner_action": "Actualizar aquí",
|
||||
"team_upgraded_successfully": "¡Tu equipo se actualizó con éxito!",
|
||||
"org_upgrade_banner_description": "Gracias por probar nuestro nuevo plan de equipo. Notamos que su equipo \"{{teamName}}\" necesita actualizarse.",
|
||||
"org_upgraded_successfully": "Su organización se actualizó con éxito.",
|
||||
"org_upgraded_successfully": "Su Organization se actualizó con éxito.",
|
||||
"use_link_to_reset_password": "Utilice el enlace de abajo para restablecer su contraseña",
|
||||
"hey_there": "Hola,",
|
||||
"forgot_your_password_calcom": "¿Olvidaste tu contraseña? - {{appName}}",
|
||||
@@ -248,7 +248,7 @@
|
||||
"organization_general_description": "Administre los ajustes para el idioma y la zona horaria de su equipo",
|
||||
"few_sentences_about_yourself": "Unas pocas frases sobre ti mismo. Esto aparecerá en tu página de url personal.",
|
||||
"nearly_there": "¡Ya casi está!",
|
||||
"nearly_there_instructions": "Lo último, una breve descripción sobre usted y una foto realmente lo ayudan a obtener reservas y a que las personas sepan con quién están reservando.",
|
||||
"nearly_there_instructions": "Lo último, una breve descripción sobre usted y una foto lo ayudan a obtener reservas y a que las personas sepan con quién están reservando.",
|
||||
"set_availability_instructions": "Defina los intervalos de tiempo cuando esté disponible de forma recurrente. Puede crear más tarde y asignarlos a diferentes calendarios.",
|
||||
"set_availability": "Establecer Disponibilidad",
|
||||
"continue_without_calendar": "Continuar sin calendario",
|
||||
@@ -306,9 +306,9 @@
|
||||
"password_has_been_reset_login": "Su contraseña ha sido restablecida. Ahora puede iniciar sesión con su nueva contraseña.",
|
||||
"layout": "Diseño",
|
||||
"bookerlayout_default_title": "Vista predeterminada",
|
||||
"bookerlayout_description": "Puede seleccionar varios y sus reservadores pueden cambiar de vista.",
|
||||
"bookerlayout_description": "Puede seleccionar varios y quienes le reserven pueden cambiar de vista.",
|
||||
"bookerlayout_user_settings_title": "Diseño de reserva",
|
||||
"bookerlayout_user_settings_description": "Puede seleccionar varios y los reservadores pueden cambiar de vista. Esto se puede anular por evento.",
|
||||
"bookerlayout_user_settings_description": "Puede seleccionar varios y quienes le reservan pueden cambiar de vista. Esto se puede anular por evento.",
|
||||
"bookerlayout_month_view": "Mes",
|
||||
"bookerlayout_week_view": "Semanal",
|
||||
"bookerlayout_column_view": "Columna",
|
||||
@@ -1327,7 +1327,7 @@
|
||||
"download_responses_description": "Descargue todas las respuestas de su formulario en formato CSV.",
|
||||
"download": "Descargar",
|
||||
"download_recording": "Descargar grabación",
|
||||
"recording_from_your_recent_call": "Una grabación de su llamada reciente en {{appName}} está lista para descargarla",
|
||||
"recording_from_your_recent_call": "Una grabación de su llamada reciente en {{appName}} está lista para descargar",
|
||||
"create_your_first_form": "Crea tu primer formulario",
|
||||
"create_your_first_form_description": "Con los formularios de orientación puedes hacer preguntas calificadoras y encaminarte hacia la persona o tipo de evento correctos.",
|
||||
"create_your_first_webhook": "Crea tu primer Webhook",
|
||||
@@ -1661,7 +1661,7 @@
|
||||
"email_user_cta": "Ver la invitación",
|
||||
"email_no_user_invite_heading": "Lo invitaron a unirse a {{appName}} {{entity}}",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}} lo invitó a unirse a su equipo en {{appName}}. {{appName}} es el programador de eventos que les permite a usted y a su equipo programar reuniones sin necesidad de enviar correos electrónicos.",
|
||||
"email_user_invite_subheading": "{{invitedBy}} lo ha invitado a unirse a su {{entity}} \"{{teamName}}\" en {{appName}}. {{appName}} es el planificador de eventos que le permite a usted y a su {{entity}} programar reuniones sin correos electrónicos de ida y vuelta.",
|
||||
"email_user_invite_subheading": "{{invitedBy}} lo ha invitado a unirse a su {{entity}} \"{{teamName}}\" en {{appName}}. {{appName}} es el planificador de eventos que le permite a usted y a su {{entity}} programar reuniones sin necesidad de correos electrónicos.",
|
||||
"email_no_user_invite_steps_intro": "Lo guiaremos a través de unos pocos pasos y disfrutará de una programación sin estrés con su {{entity}} en muy poco tiempo.",
|
||||
"email_no_user_step_one": "Elija su nombre de usuario",
|
||||
"email_no_user_step_two": "Conecte su cuenta de calendario",
|
||||
@@ -1687,7 +1687,7 @@
|
||||
"attendee_no_longer_attending": "Un asistente ya no asistirá a su evento",
|
||||
"attendee_no_longer_attending_subtitle": "{{name}} ha cancelado. Esto significa que está disponible una plaza para esta franja horaria",
|
||||
"create_event_on": "Crear evento en",
|
||||
"create_routing_form_on": "Crear formulario de enrutamiento el",
|
||||
"create_routing_form_on": "Crear formulario de enrutamiento en",
|
||||
"default_app_link_title": "Establecer un enlace de aplicación predeterminado",
|
||||
"default_app_link_description": "Establecer un enlace de aplicación predeterminado permite que todos los tipos de eventos recién creados utilicen el enlace de aplicación que establezca.",
|
||||
"organizer_default_conferencing_app": "Aplicación por defecto del organizador",
|
||||
@@ -1934,7 +1934,7 @@
|
||||
"404_the_org": "La organización",
|
||||
"404_the_team": "El equipo",
|
||||
"404_claim_entity_org": "Reclame su subdominio para su organización",
|
||||
"404_claim_entity_team": "Reclama este equipo y empiece a gestionar los horarios de forma colectiva",
|
||||
"404_claim_entity_team": "Reclame este equipo y empiece a gestionar los horarios de forma colectiva",
|
||||
"insights_all_org_filter": "Todas las aplicaciones",
|
||||
"insights_team_filter": "Equipo: {{teamName}}",
|
||||
"insights_user_filter": "Usuario: {{userName}}",
|
||||
|
||||
@@ -407,6 +407,8 @@
|
||||
"allow_dynamic_booking_tooltip": "Les liens de réservation de groupe peuvent être créés dynamiquement en ajoutant plusieurs noms d'utilisateur séparés par un « + ». Exemple : « {{appName}}/bailey+peer ».",
|
||||
"allow_dynamic_booking": "Autoriser les participants à prendre rendez-vous avec vous via des réservations de groupe dynamiques",
|
||||
"dynamic_booking": "Liens de groupe dynamiques",
|
||||
"allow_seo_indexing": "Autorisez les moteurs de recherche à accéder à votre contenu public.",
|
||||
"seo_indexing": "Autoriser l'indexation SEO",
|
||||
"email": "E-mail",
|
||||
"email_placeholder": "jdoe@exemple.com",
|
||||
"full_name": "Nom complet",
|
||||
@@ -560,6 +562,7 @@
|
||||
"leave": "Quitter",
|
||||
"profile": "Profil",
|
||||
"my_team_url": "Lien de mon équipe",
|
||||
"my_teams": "Mes équipes",
|
||||
"team_name": "Nom de l'équipe",
|
||||
"your_team_name": "Nom de votre équipe",
|
||||
"team_updated_successfully": "Équipe mise à jour avec succès",
|
||||
@@ -607,7 +610,7 @@
|
||||
"disband_org_confirmation_message": "Voulez-vous vraiment dissoudre cette organisation ? Toutes les équipes et tous les membres seront supprimés.",
|
||||
"remove_member_confirmation_message": "Voulez-vous vraiment supprimer ce membre de l'équipe ?",
|
||||
"confirm_disband_team": "Oui, dissoudre l'équipe",
|
||||
"confirm_remove_member": "Oui, retirer le membre",
|
||||
"confirm_remove_member": "Oui, supprimer le membre",
|
||||
"remove_member": "Supprimer le membre",
|
||||
"manage_your_team": "Gérez votre équipe",
|
||||
"no_teams": "Vous n'avez pas encore d'équipe.",
|
||||
@@ -869,6 +872,7 @@
|
||||
"account_managed_by_identity_provider_description": "Pour modifier votre adresse e-mail et/ou votre mot de passe, activez l'authentification à deux facteurs et plus, veuillez consulter les paramètres de votre compte {{provider}}.",
|
||||
"signin_with_google": "Se connecter avec Google",
|
||||
"signin_with_saml": "Se connecter avec SAML",
|
||||
"signin_with_saml_oidc": "Se connecter avec SAML/OIDC",
|
||||
"you_will_need_to_generate": "Vous devrez générer un token d'accès à partir de votre ancien outil de planification.",
|
||||
"import": "Importer",
|
||||
"import_from": "Importer depuis",
|
||||
@@ -926,7 +930,7 @@
|
||||
"verify_wallet": "Vérifier le portefeuille",
|
||||
"create_events_on": "Créer des événements dans :",
|
||||
"enterprise_license": "Il s'agit d'une fonctionnalité d'entreprise",
|
||||
"enterprise_license_description": "Pour activer cette fonctionnalité, demandez à un administrateur d'accéder à {{setupUrl}} pour saisir une clé de licence. Si une clé de licence est déjà en place, veuillez contacter {{supportMail}} pour obtenir de l'aide.",
|
||||
"enterprise_license_description": "Pour activer cette fonctionnalité, demandez à un administrateur d'accéder à <2>/auth/setup</2> pour saisir une clé de licence. Si une clé de licence est déjà en place, veuillez contacter <5>{{SUPPORT_MAIL_ADDRESS}}</5> pour obtenir de l'aide.",
|
||||
"enterprise_license_development": "Vous pouvez tester cette fonctionnalité en mode développement. Pour une utilisation en production, veuillez demander à un administrateur d'aller à <2>/auth/setup</2> pour entrer une clé de licence.",
|
||||
"missing_license": "Licence manquante",
|
||||
"signup_requires": "Licence commerciale requise",
|
||||
@@ -1262,6 +1266,7 @@
|
||||
"error_updating_settings": "Erreur lors de la mise à jour des paramètres",
|
||||
"personal_cal_url": "Mon lien {{appName}} personnel",
|
||||
"bio_hint": "Quelques mots à propos de vous. Ces informations apparaîtront sur votre page publique.",
|
||||
"user_has_no_bio": "Cet utilisateur n'a pas encore ajouté de description.",
|
||||
"delete_account_modal_title": "Supprimer le compte",
|
||||
"confirm_delete_account_modal": "Voulez-vous vraiment supprimer votre compte {{appName}} ?",
|
||||
"delete_my_account": "Supprimer mon compte",
|
||||
@@ -1978,6 +1983,11 @@
|
||||
"org_team_names_example_5": "p. ex. Équipe d'analyse de données",
|
||||
"org_max_team_warnings": "Vous pourrez ajouter plus d'équipes plus tard.",
|
||||
"what_is_this_meeting_about": "En quoi consiste ce rendez-vous ?",
|
||||
"add_to_team": "Ajouter à l'équipe",
|
||||
"remove_users_from_org": "Supprimer des utilisateurs de l'organisation",
|
||||
"remove_users_from_org_confirm": "Voulez-vous vraiment supprimer {{userCount}} utilisateurs de cette organisation ?",
|
||||
"user_has_no_schedules": "Cet utilisateur n'a pas encore configuré de planning",
|
||||
"user_isnt_in_any_teams": "Cet utilisateur n'est dans aucune équipe",
|
||||
"requires_confirmation_mandatory": "Les messages ne peuvent être envoyés aux participants que lorsque le type d'événement nécessite une confirmation.",
|
||||
"kyc_verification_information": "Pour garantir la sécurité, vous devez vérifier votre {{teamOrAccount}} avant d'envoyer des messages aux participants. Veuillez nous contacter à <a>{{supportEmail}}</a> et fournir les informations suivantes :",
|
||||
"kyc_verification_documents": "<ul><li>Votre {{teamOrUser}}</li><li>Pour les entreprises : Joignez votre document de vérification d'entreprise</li><li>Pour les particuliers : Joignez une pièce d'identité</li></ul>",
|
||||
@@ -1993,5 +2003,20 @@
|
||||
"attendee_last_name_variable": "Nom du participant",
|
||||
"me": "Moi",
|
||||
"verify_team_tooltip": "Vérifiez votre équipe pour activer l'envoi de messages aux participants",
|
||||
"member_removed": "Membre supprimé",
|
||||
"my_availability": "Mes disponibilités",
|
||||
"team_availability": "Disponibilités de l'équipe",
|
||||
"admin_org_notification_email_subject": "Nouvelle organisation créée : action en attente",
|
||||
"admin_org_notification_email_title": "Une organisation nécessite une configuration DNS",
|
||||
"admin_org_notification_email_body_part2": "Ou l'enregistrement CNAME :",
|
||||
"admin_org_notification_email_body_part3": "Une fois le sous-domaine configuré, veuillez marquer la configuration DNS comme terminée dans les paramètres d'administration des organisations.",
|
||||
"admin_org_notification_email_cta": "Accéder aux paramètres d'administration des organisations",
|
||||
"org_error_processing": "Une erreur s'est produite lors du traitement de cette organisation",
|
||||
"orgs_page_description": "Une liste de toutes les organisations. Accepter une organisation permettra à tous les utilisateurs avec ce domaine de messagerie de s'inscrire SANS vérification d'e-mail.",
|
||||
"unverified": "Non vérifié",
|
||||
"dns_missing": "DNS manquant",
|
||||
"mark_dns_configured": "Marquer comme DNS configuré",
|
||||
"value": "Valeur",
|
||||
"your_organization_updated_sucessfully": "Votre organisation a été mise à jour avec succès",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user