Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c389c307c | ||
|
|
dae4627ada | ||
|
|
83d98a1ac4 | ||
|
|
bd40dcfa4d | ||
|
|
9dc4c84cfd | ||
|
|
69c01b69f6 | ||
|
|
424edc7437 | ||
|
|
50d2dad62c | ||
|
|
06dcaaef3b | ||
|
|
ad3bdbbda7 | ||
|
|
533371da20 | ||
|
|
94b4507901 | ||
|
|
829a103f6b | ||
|
|
db80afce40 | ||
|
|
6b9d2331fc | ||
|
|
2011a1bd38 | ||
|
|
6a16366941 | ||
|
|
5a9ee2047f | ||
|
|
3a80c82023 | ||
|
|
1d94552f90 | ||
|
|
620a485fd0 | ||
|
|
eb93e81baf | ||
|
|
47739a7636 | ||
|
|
10bd332405 | ||
|
|
35be4182e9 | ||
|
|
30095d0428 | ||
|
|
5a9084332f | ||
|
|
d897e72ce2 | ||
|
|
582a545e67 | ||
|
|
6340cb9de1 | ||
|
|
89107d82e7 | ||
|
|
1851a3c1cf | ||
|
|
53cda97c6e | ||
|
|
f92f0ce1fa | ||
|
|
018eb59c9d | ||
|
|
03d8ef9c03 | ||
|
|
d10071a4a2 | ||
|
|
27aaf3f75a | ||
|
|
6ff4e07d8f | ||
|
|
b740423eb8 | ||
|
|
c0f21be255 | ||
|
|
0e62c411f8 | ||
|
|
c89b217c92 | ||
|
|
d3bbe8784f | ||
|
|
f168ec8df6 | ||
|
|
0138fdd971 | ||
|
|
365b95e28a | ||
|
|
0ea76332db | ||
|
|
43e3ef170a | ||
|
|
9202b53e49 | ||
|
|
41850c7f59 | ||
|
|
bd4f99a850 | ||
|
|
771f0b67db | ||
|
|
0f0b55488f | ||
|
|
4e1e586541 | ||
|
|
80a1458538 | ||
|
|
8de75aebf0 | ||
|
|
a9cc3649e0 | ||
|
|
2b5577772b | ||
|
|
f081c733b8 | ||
|
|
3ed5a5552a | ||
|
|
61f1a3b21e | ||
|
|
d353904c7c | ||
|
|
8e192ac358 | ||
|
|
85c3c6dfc0 | ||
|
|
1aa3077706 | ||
|
|
337192697f | ||
|
|
299f74c1fb | ||
|
|
d55404b3aa | ||
|
|
1bb5b0f76c | ||
|
|
2a8eca7885 | ||
|
|
1566a80e40 | ||
|
|
797d3a7f98 | ||
|
|
c7a276eb1d | ||
|
|
1709e23135 | ||
|
|
d1778fdce8 | ||
|
|
b49952b8d8 | ||
|
|
5b280b2188 | ||
|
|
3818093652 | ||
|
|
56f4310c24 | ||
|
|
7ec7195ae9 | ||
|
|
514db3466f | ||
|
|
cd88b688f4 | ||
|
|
939a32813f | ||
|
|
66c1e29d91 | ||
|
|
3c57082fa6 | ||
|
|
fc4a2bd186 | ||
|
|
3f900966b0 | ||
|
|
8ba4b4d50e | ||
|
|
d2104dac5a | ||
|
|
9ad2731358 | ||
|
|
c34fdab5af | ||
|
|
f1c981fdcf | ||
|
|
47f65bb19f | ||
|
|
587d3c1088 | ||
|
|
610c86837a | ||
|
|
676452b4f4 | ||
|
|
8a432b45a4 | ||
|
|
92ad652666 | ||
|
|
617ddcaa3b | ||
|
|
ef7f3e7900 | ||
|
|
4e0a549a50 | ||
|
|
8f95d27469 | ||
|
|
8902f7efa1 | ||
|
|
bd7eadd721 | ||
|
|
02e34a7bde | ||
|
|
d51e1ab47d |
@@ -5,7 +5,7 @@ on:
|
||||
jobs:
|
||||
e2e-app-store:
|
||||
timeout-minutes: 20
|
||||
name: E2E App-Store Apps
|
||||
name: E2E App Store (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
@@ -15,6 +15,11 @@ jobs:
|
||||
POSTGRES_DB: calendso
|
||||
ports:
|
||||
- 5432:5432
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
## There aren't many tests for AppStore. So, just start with 2 shards. Increase if needed.
|
||||
shard: [1, 2]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -26,12 +31,12 @@ jobs:
|
||||
- uses: ./.github/actions/cache-db
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: yarn test-e2e:app-store
|
||||
run: yarn e2e:app-store --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
env:
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results
|
||||
name: app-store-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||
path: app-store-results
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
name: E2E Embed tests and booking flow(for non-embed as well)
|
||||
name: E2E Embed React tests and booking flow(for non-embed as well)
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
e2e-embed:
|
||||
timeout-minutes: 20
|
||||
name: E2E Embed React (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
@@ -14,6 +15,11 @@ jobs:
|
||||
POSTGRES_DB: calendso
|
||||
ports:
|
||||
- 5432:5432
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
## There aren't many tests for embed-react. So, just start with 2 shards. Increase if needed.
|
||||
shard: [1, 2]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -26,7 +32,7 @@ jobs:
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: |
|
||||
yarn test-e2e:embed-react
|
||||
yarn e2e:embed-react --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
yarn workspace @calcom/embed-react packaged:tests
|
||||
env:
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
@@ -34,5 +40,5 @@ jobs:
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results
|
||||
name: embed-react-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||
path: embed-react-results
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
name: E2E Embed tests and booking flow(for non-embed as well)
|
||||
name: E2E Embed Core tests and booking flow(for non-embed as well)
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
e2e-embed:
|
||||
timeout-minutes: 20
|
||||
name: E2E Embed Core (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
@@ -14,6 +15,11 @@ jobs:
|
||||
POSTGRES_DB: calendso
|
||||
ports:
|
||||
- 5432:5432
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
## There aren't many tests for embed-core. So, just start with 2 shards. Increase if needed.
|
||||
shard: [1, 2]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -25,12 +31,12 @@ jobs:
|
||||
- uses: ./.github/actions/cache-db
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: yarn test-e2e:embed
|
||||
run: yarn e2e:embed --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
env:
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results
|
||||
name: embed-core-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||
path: embed-core-results
|
||||
|
||||
@@ -84,21 +84,18 @@ jobs:
|
||||
|
||||
e2e-app-store:
|
||||
name: E2E App Store tests
|
||||
if: ${{ needs.changes.outputs.app-store == 'true' }}
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-app-store.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed:
|
||||
name: E2E embeds tests
|
||||
if: ${{ needs.changes.outputs.embed == 'true' }}
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-embed.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed-react:
|
||||
name: E2E React embeds tests
|
||||
if: ${{ needs.changes.outputs.embed-react == 'true' }}
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
secrets: inherit
|
||||
@@ -109,7 +106,7 @@ jobs:
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [lint, type-check, test, build, e2e]
|
||||
needs: [lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
if: always()
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
name: "Release Docker"
|
||||
|
||||
on: # yamllint disable-line rule:truthy
|
||||
release:
|
||||
types:
|
||||
- created
|
||||
# in case manual trigger is needed
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
RELEASE_TAG:
|
||||
description: 'v{Major}.{Minor}.{Patch}'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: "Remote Release"
|
||||
|
||||
runs-on: "ubuntu-latest"
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: "Determine tag"
|
||||
run: "echo \"RELEASE_TAG=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV"
|
||||
|
||||
- name: "Run remote release workflow"
|
||||
uses: "actions/github-script@v6"
|
||||
with:
|
||||
# Requires a personal access token with Actions Read and write permissions on calcom/docker.
|
||||
github-token: "${{ secrets.DOCKER_REPO_ACCESS_TOKEN }}"
|
||||
script: |
|
||||
try {
|
||||
const response = await github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: 'docker',
|
||||
workflow_id: 'create-release.yaml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
"RELEASE_TAG": process.env.RELEASE_TAG
|
||||
},
|
||||
});
|
||||
|
||||
console.log(response);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
@@ -57,7 +57,14 @@ import { defaultResponder } from "@calcom/lib/server";
|
||||
* format: email
|
||||
* description: 'Attendee email address'
|
||||
* location:
|
||||
* type: string
|
||||
* type: object
|
||||
* properties:
|
||||
* optionValue:
|
||||
* type: string
|
||||
* description: 'Option value for the location'
|
||||
* value:
|
||||
* type: string
|
||||
* description: 'The meeting URL, Phone number or Address'
|
||||
* description: 'Meeting location'
|
||||
* metadata:
|
||||
* type: object
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
# @calcom/web
|
||||
|
||||
## 3.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @calcom/embed-react@1.3.0
|
||||
|
||||
## 3.0.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @calcom/embed-snippet@1.1.2
|
||||
- @calcom/embed-react@1.2.2
|
||||
- @calcom/embed-core@1.3.2
|
||||
|
||||
## 3.0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @calcom/embed-snippet@1.1.1
|
||||
- @calcom/embed-react@1.2.1
|
||||
- @calcom/embed-core@1.3.1
|
||||
|
||||
## 3.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @calcom/embed-core@1.3.0
|
||||
- @calcom/embed-react@1.2.0
|
||||
- @calcom/embed-snippet@1.1.0
|
||||
|
||||
## 2.9.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
+757
-260
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ import type { IframeHTMLAttributes } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
|
||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import { InstallAppButton, AppDependencyComponent } from "@calcom/app-store/components";
|
||||
@@ -217,7 +217,7 @@ const Component = ({
|
||||
};
|
||||
}
|
||||
return (
|
||||
<InstallAppButtonChild appCategories={categories} userAdminTeams={appDbQuery.data?.userAdminTeams} addAppMutationInput={{type, variant, slug}} credentials={appDbQuery.data?.credentials} {...props} />
|
||||
<InstallAppButtonChild appCategories={categories} userAdminTeams={appDbQuery.data?.userAdminTeams} addAppMutationInput={{ type, variant, slug }} credentials={appDbQuery.data?.credentials} {...props} />
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -416,7 +416,7 @@ const InstallAppButtonChild = ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!userAdminTeams?.length || appCategories.some((category) => category === "calendar")) {
|
||||
if (!userAdminTeams?.length || appCategories.some((category) => ["calendar", "conferencing"].includes(category))) {
|
||||
return <Button
|
||||
data-testid="install-app-button"
|
||||
{...props}
|
||||
@@ -426,7 +426,7 @@ const InstallAppButtonChild = ({
|
||||
size="base">
|
||||
{multiInstall ? t("install_another") : t("install_app")}
|
||||
</Button>
|
||||
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -444,49 +444,50 @@ const InstallAppButtonChild = ({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent
|
||||
onInteractOutside={(event) => {
|
||||
if (mutation.isLoading) event.preventDefault();
|
||||
}}
|
||||
onInteractOutside={(event) => {
|
||||
if (mutation.isLoading) event.preventDefault();
|
||||
}}
|
||||
>
|
||||
{mutation.isLoading && (
|
||||
<div className="z-1 fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<div className="z-1 fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<DropdownMenuLabel>{t("install_app_on")}</DropdownMenuLabel>
|
||||
{userAdminTeams.map((team) => {
|
||||
|
||||
|
||||
const isInstalled = credentials &&
|
||||
credentials.some((credential) =>
|
||||
credential?.teamId ? credential?.teamId === team.id : credential.userId === team.id
|
||||
)
|
||||
credentials.some((credential) =>
|
||||
credential?.teamId ? credential?.teamId === team.id : credential.userId === team.id
|
||||
)
|
||||
|
||||
return (
|
||||
<DropdownItem
|
||||
type="button"
|
||||
data-testid={team.isUser ? "install-app-button-personal" : "anything else"}
|
||||
key={team.id}
|
||||
disabled={
|
||||
isInstalled
|
||||
}
|
||||
StartIcon={(props) => (
|
||||
<Avatar
|
||||
alt={team.logo || ""}
|
||||
imageSrc={team.logo || `${CAL_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
|
||||
size="sm"
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
onClick={() => {
|
||||
mutation.mutate(
|
||||
team.isUser ? addAppMutationInput : { ...addAppMutationInput, teamId: team.id }
|
||||
);
|
||||
}}>
|
||||
<p>{team.name}{" "}
|
||||
{isInstalled &&
|
||||
`(${t("installed")})`}</p>
|
||||
</DropdownItem>
|
||||
)})}
|
||||
<DropdownItem
|
||||
type="button"
|
||||
data-testid={team.isUser ? "install-app-button-personal" : "anything else"}
|
||||
key={team.id}
|
||||
disabled={
|
||||
isInstalled
|
||||
}
|
||||
StartIcon={(props) => (
|
||||
<Avatar
|
||||
alt={team.logo || ""}
|
||||
imageSrc={team.logo || `${CAL_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
|
||||
size="sm"
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
onClick={() => {
|
||||
mutation.mutate(
|
||||
team.isUser ? addAppMutationInput : { ...addAppMutationInput, teamId: team.id }
|
||||
);
|
||||
}}>
|
||||
<p>{team.name}{" "}
|
||||
{isInstalled &&
|
||||
`(${t("installed")})`}</p>
|
||||
</DropdownItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</Dropdown>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
import { z } from "zod";
|
||||
import type { z } from "zod";
|
||||
|
||||
import type { EventNameObjectType } from "@calcom/core/event";
|
||||
import { getEventName } from "@calcom/core/event";
|
||||
@@ -17,14 +17,23 @@ import {
|
||||
allowDisablingHostConfirmationEmails,
|
||||
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
|
||||
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||
import { EditableSchema } from "@calcom/features/form-builder/FormBuilderFieldsSchema";
|
||||
import type { EditableSchema } from "@calcom/features/form-builder/schema";
|
||||
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip, Alert } from "@calcom/ui";
|
||||
import {
|
||||
Button,
|
||||
CheckboxField,
|
||||
Label,
|
||||
SettingsToggle,
|
||||
showToast,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Alert,
|
||||
} from "@calcom/ui";
|
||||
import { Edit, Copy } from "@calcom/ui/components/icon";
|
||||
|
||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||
@@ -342,7 +351,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
}}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<Checkbox
|
||||
<CheckboxField
|
||||
description={t("show_attendees")}
|
||||
disabled={seatsLocked.disabled}
|
||||
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
@@ -158,9 +160,21 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
{!shouldLockDisableProps("apps").disabled && (
|
||||
<div className="bg-muted rounded-md p-8">
|
||||
{!isLoading && notInstalledApps?.length ? (
|
||||
<h2 className="text-emphasis text-lg font-semibold">{t("available_apps")}</h2>
|
||||
<>
|
||||
<h2 className="text-emphasis mb-2 text-xl font-semibold leading-5 tracking-[0.01em]">
|
||||
{t("available_apps_lower_case")}
|
||||
</h2>
|
||||
<p className="text-default mb-6 text-sm font-normal">
|
||||
<Trans i18nKey="available_apps_desc">
|
||||
You have no apps installed. View popular apps below and explore more in our
|
||||
<Link className="cursor-pointer underline" href="/apps">
|
||||
App Store
|
||||
</Link>
|
||||
</Trans>
|
||||
</p>
|
||||
</>
|
||||
) : null}
|
||||
<div className="before:border-0">
|
||||
<div className="bg-default border-subtle divide-subtle divide-y rounded-md border before:border-0">
|
||||
{notInstalledApps?.map((app) => (
|
||||
<EventTypeAppCard
|
||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||
|
||||
@@ -19,16 +19,17 @@ interface IUserToValue {
|
||||
email: string;
|
||||
}
|
||||
|
||||
const mapUserToValue = ({ id, name, username, email }: IUserToValue) => ({
|
||||
const mapUserToValue = ({ id, name, username, email }: IUserToValue, pendingString: string) => ({
|
||||
value: `${id || ""}`,
|
||||
label: `${name || ""}`,
|
||||
label: `${name || email || ""}${!username ? ` (${pendingString})` : ""}`,
|
||||
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
|
||||
email,
|
||||
});
|
||||
|
||||
export const mapMemberToChildrenOption = (
|
||||
member: EventTypeSetupProps["teamMembers"][number],
|
||||
slug: string
|
||||
slug: string,
|
||||
pendingString: string
|
||||
) => {
|
||||
return {
|
||||
slug,
|
||||
@@ -43,7 +44,7 @@ export const mapMemberToChildrenOption = (
|
||||
eventTypeSlugs: member.eventTypes ?? [],
|
||||
},
|
||||
value: `${member.id ?? ""}`,
|
||||
label: member.name ?? "",
|
||||
label: `${member.name || member.email || ""}${!member.username ? ` (${pendingString})` : ""}`,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -291,9 +292,13 @@ export const EventTeamTab = ({
|
||||
// description: t("round_robin_description"),
|
||||
},
|
||||
];
|
||||
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
||||
const childrenEventTypeOptions = teamMembers.map((member) => {
|
||||
return mapMemberToChildrenOption(member, eventType.slug);
|
||||
const pendingMembers = (member: (typeof teamMembers)[number]) =>
|
||||
!!eventType.team?.parentId || !!member.username;
|
||||
const teamMembersOptions = teamMembers
|
||||
.filter(pendingMembers)
|
||||
.map((member) => mapUserToValue(member, t("pending")));
|
||||
const childrenEventTypeOptions = teamMembers.filter(pendingMembers).map((member) => {
|
||||
return mapMemberToChildrenOption(member, eventType.slug, t("pending"));
|
||||
});
|
||||
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
|
||||
return (
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useMemo, useState, Suspense } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { useOrgBrandingValues } from "@calcom/features/ee/organizations/hooks";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
@@ -209,10 +209,8 @@ function EventTypeSingleLayout({
|
||||
}`,
|
||||
});
|
||||
}
|
||||
if (isManagedEventType || isChildrenManagedEventType) {
|
||||
// Removing apps and workflows for manageg event types by admins v1
|
||||
navigation.splice(0, 1);
|
||||
} else {
|
||||
const showWebhooks = !(isManagedEventType || isChildrenManagedEventType);
|
||||
if (showWebhooks) {
|
||||
navigation.push({
|
||||
name: "webhooks",
|
||||
href: `/event-types/${eventType.id}?tabName=webhooks`,
|
||||
@@ -234,7 +232,7 @@ function EventTypeSingleLayout({
|
||||
formMethods,
|
||||
]);
|
||||
|
||||
const orgBranding = useOrgBrandingValues();
|
||||
const orgBranding = useOrgBranding();
|
||||
const isOrgEvent = orgBranding?.fullDomain;
|
||||
const permalink = `${orgBranding?.fullDomain ?? CAL_URL}/${
|
||||
team ? `${!isOrgEvent ? "team/" : ""}${team.slug}` : eventType.users[0].username
|
||||
@@ -318,6 +316,7 @@ function EventTypeSingleLayout({
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
tooltip={t("embed")}
|
||||
eventId={eventType.id}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||
|
||||
interface IUserSettingsProps {
|
||||
nextStep: () => void;
|
||||
hideUsername?: boolean;
|
||||
}
|
||||
|
||||
const UserSettings = (props: IUserSettingsProps) => {
|
||||
@@ -66,8 +67,8 @@ const UserSettings = (props: IUserSettingsProps) => {
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="space-y-6">
|
||||
{/* Username textfield */}
|
||||
<UsernameAvailabilityField />
|
||||
{/* Username textfield: when not coming from signup */}
|
||||
{!props.hideUsername && <UsernameAvailabilityField />}
|
||||
|
||||
{/* Full name textfield */}
|
||||
<div className="w-full">
|
||||
|
||||
@@ -12,7 +12,7 @@ import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
|
||||
interface DisableTwoFactorAuthModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: () => void;
|
||||
|
||||
disablePassword?: boolean;
|
||||
/** Called when the user closes the modal without disabling two-factor auth */
|
||||
onCancel: () => void;
|
||||
/** Called when the user disables two-factor auth */
|
||||
@@ -27,6 +27,7 @@ interface DisableTwoFactorValues {
|
||||
const DisableTwoFactorAuthModal = ({
|
||||
onDisable,
|
||||
onCancel,
|
||||
disablePassword,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: DisableTwoFactorAuthModalProps) => {
|
||||
@@ -75,19 +76,21 @@ const DisableTwoFactorAuthModal = ({
|
||||
<DialogContent title={t("disable_2fa")} description={t("disable_2fa_recommendation")} type="creation">
|
||||
<Form form={form} handleSubmit={handleDisable}>
|
||||
<div className="mb-8">
|
||||
<PasswordField
|
||||
labelProps={{
|
||||
className: "block text-sm font-medium text-default",
|
||||
}}
|
||||
{...form.register("password")}
|
||||
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
|
||||
/>
|
||||
{!disablePassword && (
|
||||
<PasswordField
|
||||
labelProps={{
|
||||
className: "block text-sm font-medium text-default",
|
||||
}}
|
||||
{...form.register("password")}
|
||||
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
|
||||
/>
|
||||
)}
|
||||
<TwoFactor center={false} />
|
||||
|
||||
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
|
||||
</div>
|
||||
|
||||
<DialogFooter showDivider className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<DialogFooter showDivider className="relative mt-5">
|
||||
<Button color="secondary" onClick={onCancel}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
|
||||
@@ -185,7 +185,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
|
||||
)}
|
||||
</div>
|
||||
</WithStep>
|
||||
<DialogFooter className="mt-8 sm:flex sm:flex-row-reverse" showDivider>
|
||||
<DialogFooter className="mt-8" showDivider>
|
||||
<Button color="secondary" onClick={onCancel}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { EventCollectionProvider } from "next-collect/client";
|
||||
import type { SSRConfig } from "next-i18next";
|
||||
import { appWithTranslation } from "next-i18next";
|
||||
@@ -10,7 +11,6 @@ import { useRouter } from "next/router";
|
||||
import type { ComponentProps, PropsWithChildren, ReactNode } from "react";
|
||||
|
||||
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { useOrgBrandingValues } from "@calcom/features/ee/organizations/hooks";
|
||||
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
|
||||
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
||||
import { FeatureProvider } from "@calcom/features/flags/context/provider";
|
||||
@@ -218,9 +218,26 @@ function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
|
||||
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
|
||||
}
|
||||
|
||||
function useOrgBrandingValues() {
|
||||
const session = useSession();
|
||||
|
||||
const res = trpc.viewer.organizations.getBrand.useQuery(undefined, {
|
||||
// Only fetch if we have a session to avoid flooding logs with errors
|
||||
enabled: session.status === "authenticated",
|
||||
});
|
||||
|
||||
if (res.status === "loading") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (res.status === "error") return null;
|
||||
|
||||
return res.data;
|
||||
}
|
||||
|
||||
function OrgBrandProvider({ children }: { children: React.ReactNode }) {
|
||||
const orgBrand = useOrgBrandingValues();
|
||||
return <OrgBrandingProvider value={orgBrand}>{children}</OrgBrandingProvider>;
|
||||
return <OrgBrandingProvider value={{ orgBrand }}>{children}</OrgBrandingProvider>;
|
||||
}
|
||||
|
||||
const AppProviders = (props: AppPropsWithChildren) => {
|
||||
|
||||
@@ -78,7 +78,7 @@ const middleware: NextMiddleware = async (req) => {
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/:path*",
|
||||
"/((?!_next|.*avatar.png$|favicon.ico$).*)",
|
||||
"/api/collect-events/:path*",
|
||||
"/api/auth/:path*",
|
||||
"/apps/routing_forms/:path*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.1.3",
|
||||
"version": "3.1.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
+32
-116
@@ -13,12 +13,7 @@ import {
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
||||
import defaultEvents, {
|
||||
getDynamicEventDescription,
|
||||
getGroupName,
|
||||
getUsernameList,
|
||||
getUsernameSlugLink,
|
||||
} from "@calcom/lib/defaultEvents";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
@@ -26,7 +21,7 @@ import { stripMarkdown } from "@calcom/lib/stripMarkdown";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui";
|
||||
import { Avatar, HeadSeo } from "@calcom/ui";
|
||||
import { Verified, ArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
@@ -38,16 +33,7 @@ import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export type UserPageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||
export function UserPage(props: UserPageProps) {
|
||||
const {
|
||||
users,
|
||||
profile,
|
||||
eventTypes,
|
||||
isDynamicGroup,
|
||||
dynamicNames,
|
||||
dynamicUsernames,
|
||||
isSingleUser,
|
||||
markdownStrippedBio,
|
||||
} = props;
|
||||
const { users, profile, eventTypes, isSingleUser, markdownStrippedBio } = props;
|
||||
const [user] = users; //To be used when we only have a single user, not dynamic group
|
||||
useTheme(user.theme);
|
||||
const { t } = useLocale();
|
||||
@@ -55,47 +41,6 @@ export function UserPage(props: UserPageProps) {
|
||||
|
||||
const isBioEmpty = !user.bio || !user.bio.replace("<p><br></p>", "").length;
|
||||
|
||||
const groupEventTypes = props.users.some((user) => !user.allowDynamicBooking) ? (
|
||||
<div className="space-y-6" data-testid="event-types">
|
||||
<div className="overflow-hidden rounded-sm border ">
|
||||
<div className="text-muted p-8 text-center">
|
||||
<h2 className="font-cal text-default mb-2 text-3xl">{" " + t("unavailable")}</h2>
|
||||
<p className="mx-auto max-w-md">{t("user_dynamic_booking_disabled") as string}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ul>
|
||||
{eventTypes.map((type, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className=" border-subtle bg-default dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
|
||||
<ArrowRight className="text-emphasis absolute right-3 top-3 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
<Link
|
||||
href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
|
||||
className="flex justify-between px-6 py-4"
|
||||
data-testid="event-type-link">
|
||||
<div className="flex-shrink">
|
||||
<p className=" text-emphasis text-sm font-semibold">{type.title}</p>
|
||||
<EventTypeDescription className="text-sm" eventType={type} />
|
||||
</div>
|
||||
<div className="mt-1 self-center">
|
||||
<AvatarGroup
|
||||
truncateAfter={4}
|
||||
className="flex flex-shrink-0"
|
||||
size="sm"
|
||||
items={props.users.map((user) => ({
|
||||
alt: user.name || "",
|
||||
image: user.avatar,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
const isEmbed = useIsEmbed(props.isEmbed);
|
||||
const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem");
|
||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||
@@ -117,14 +62,12 @@ export function UserPage(props: UserPageProps) {
|
||||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
title={isDynamicGroup ? dynamicNames.join(", ") : nameOrUsername}
|
||||
description={isDynamicGroup ? `Book events with ${dynamicUsernames.join(", ")}` : markdownStrippedBio}
|
||||
title={nameOrUsername}
|
||||
description={markdownStrippedBio}
|
||||
meeting={{
|
||||
title: isDynamicGroup ? "" : markdownStrippedBio,
|
||||
title: markdownStrippedBio,
|
||||
profile: { name: `${profile.name}`, image: null },
|
||||
users: isDynamicGroup
|
||||
? dynamicUsernames.map((username, index) => ({ username, name: dynamicNames[index] }))
|
||||
: [{ username: `${user.username}`, name: `${user.name}` }],
|
||||
users: [{ username: `${user.username}`, name: `${user.name}` }],
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -165,8 +108,6 @@ export function UserPage(props: UserPageProps) {
|
||||
<p className="mx-auto max-w-md">{t("user_away_description") as string}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : isDynamicGroup ? ( //When we deal with dynamic group (users > 1)
|
||||
groupEventTypes
|
||||
) : (
|
||||
eventTypes.map((type) => (
|
||||
<div
|
||||
@@ -288,6 +229,21 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
},
|
||||
});
|
||||
|
||||
const isDynamicGroup = usersWithoutAvatar.length > 1;
|
||||
if (isDynamicGroup) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: `/${usernameList.join("+")}/dynamic`,
|
||||
},
|
||||
} as {
|
||||
redirect: {
|
||||
permanent: false;
|
||||
destination: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const users = usersWithoutAvatar.map((user) => ({
|
||||
...user,
|
||||
avatar: `/${user.username}/avatar.png`,
|
||||
@@ -300,45 +256,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
notFound: true;
|
||||
};
|
||||
}
|
||||
const isDynamicGroup = users.length > 1;
|
||||
|
||||
if (isDynamicGroup) {
|
||||
// sort and be in the same order as usernameList so first user is the first user in the list
|
||||
users.sort((a, b) => {
|
||||
const aIndex = (a.username && usernameList.indexOf(a.username)) || 0;
|
||||
const bIndex = (b.username && usernameList.indexOf(b.username)) || 0;
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
}
|
||||
|
||||
const dynamicNames = isDynamicGroup
|
||||
? users.map((user) => {
|
||||
return user.name || "";
|
||||
})
|
||||
: [];
|
||||
const [user] = users; //to be used when dealing with single user, not dynamic group
|
||||
|
||||
const profile = isDynamicGroup
|
||||
? {
|
||||
name: getGroupName(dynamicNames),
|
||||
image: null,
|
||||
theme: null,
|
||||
weekStart: "Sunday",
|
||||
brandColor: "",
|
||||
darkBrandColor: "",
|
||||
allowDynamicBooking: !users.some((user) => {
|
||||
return !user.allowDynamicBooking;
|
||||
}),
|
||||
}
|
||||
: {
|
||||
name: user.name || user.username,
|
||||
image: user.avatar,
|
||||
theme: user.theme,
|
||||
brandColor: user.brandColor,
|
||||
darkBrandColor: user.darkBrandColor,
|
||||
};
|
||||
const profile = {
|
||||
name: user.name || user.username,
|
||||
image: user.avatar,
|
||||
theme: user.theme,
|
||||
brandColor: user.brandColor,
|
||||
darkBrandColor: user.darkBrandColor,
|
||||
};
|
||||
|
||||
const eventTypesWithHidden = isDynamicGroup ? [] : await getEventTypesWithHiddenFromDB(user.id);
|
||||
const eventTypesWithHidden = await getEventTypesWithHiddenFromDB(user.id);
|
||||
const dataFetchEnd = Date.now();
|
||||
if (context.query.log === "1") {
|
||||
context.res.setHeader("X-Data-Fetch-Time", `${dataFetchEnd - dataFetchStart}ms`);
|
||||
@@ -352,11 +281,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
}));
|
||||
|
||||
const isSingleUser = users.length === 1;
|
||||
const dynamicUsernames = isDynamicGroup
|
||||
? users.map((user) => {
|
||||
return user.username || "";
|
||||
})
|
||||
: [];
|
||||
|
||||
const safeBio = markdownToSafeHTML(user.bio) || "";
|
||||
|
||||
@@ -368,20 +292,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
safeBio,
|
||||
profile,
|
||||
// Dynamic group has no theme preference right now. It uses system theme.
|
||||
themeBasis: isDynamicGroup ? null : user.username,
|
||||
themeBasis: user.username,
|
||||
user: {
|
||||
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
||||
},
|
||||
eventTypes: isDynamicGroup
|
||||
? defaultEvents.map((event) => {
|
||||
event.description = getDynamicEventDescription(dynamicUsernames, event.slug);
|
||||
return event;
|
||||
})
|
||||
: eventTypes,
|
||||
eventTypes,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isDynamicGroup,
|
||||
dynamicNames,
|
||||
dynamicUsernames,
|
||||
isSingleUser,
|
||||
markdownStrippedBio,
|
||||
},
|
||||
|
||||
@@ -2,25 +2,34 @@ import type { GetServerSidePropsContext } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Booker } from "@calcom/atoms";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||
|
||||
export default function Type({ slug, user, booking, away, isBrandingHidden, rescheduleUid, org }: PageProps) {
|
||||
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
|
||||
export default function Type({
|
||||
slug,
|
||||
user,
|
||||
isEmbed,
|
||||
booking,
|
||||
away,
|
||||
isBrandingHidden,
|
||||
rescheduleUid,
|
||||
org,
|
||||
}: PageProps) {
|
||||
return (
|
||||
<main className={classNames("flex h-full items-center justify-center", !isEmbed && "min-h-[100dvh]")}>
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
<BookerSeo
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
|
||||
@@ -2,8 +2,10 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { checkPremiumUsername } from "@calcom/ee/common/lib/checkPremiumUsername";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
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";
|
||||
@@ -12,7 +14,9 @@ import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
const signupSchema = z.object({
|
||||
username: z.string(),
|
||||
username: z.string().refine((value) => !value.includes("+"), {
|
||||
message: "String should not contain a plus symbol (+).",
|
||||
}),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(7),
|
||||
language: z.string().optional(),
|
||||
@@ -21,7 +25,7 @@ const signupSchema = z.object({
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return;
|
||||
return res.status(405).end();
|
||||
}
|
||||
|
||||
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true") {
|
||||
@@ -40,37 +44,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 });
|
||||
}
|
||||
|
||||
let foundToken: { id: number; teamId: number | null; expires: Date } | null = null;
|
||||
if (token) {
|
||||
foundToken = await prisma.verificationToken.findFirst({
|
||||
@@ -98,26 +71,40 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(409).json({ message: "Username 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 hashedPassword = await hashPassword(password);
|
||||
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email: userEmail },
|
||||
update: {
|
||||
username,
|
||||
password: hashedPassword,
|
||||
emailVerified: new Date(Date.now()),
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
},
|
||||
create: {
|
||||
username,
|
||||
email: userEmail,
|
||||
password: hashedPassword,
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
},
|
||||
});
|
||||
|
||||
if (foundToken && foundToken?.teamId) {
|
||||
const team = await prisma.team.findUnique({
|
||||
where: {
|
||||
@@ -127,6 +114,34 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
if (team) {
|
||||
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
|
||||
|
||||
if (IS_CALCOM && (!teamMetadata?.isOrganization || !!team.parentId)) {
|
||||
const checkUsername = await checkPremiumUsername(username);
|
||||
if (checkUsername.premium) {
|
||||
// This signup page is ONLY meant for team invites and local setup. Not for every day users.
|
||||
// In singup redesign/refactor coming up @sean will tackle this to make them the same API/page instead of two.
|
||||
return res.status(422).json({
|
||||
message: "Sign up from https://cal.com/signup to claim your premium username",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email: userEmail },
|
||||
update: {
|
||||
username,
|
||||
password: hashedPassword,
|
||||
emailVerified: new Date(Date.now()),
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
},
|
||||
create: {
|
||||
username,
|
||||
email: userEmail,
|
||||
password: hashedPassword,
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
},
|
||||
});
|
||||
|
||||
if (teamMetadata?.isOrganization) {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
@@ -196,13 +211,37 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
id: foundToken.id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
if (IS_CALCOM) {
|
||||
const checkUsername = await checkPremiumUsername(username);
|
||||
if (checkUsername.premium) {
|
||||
res.status(422).json({
|
||||
message: "Sign up from https://cal.com/signup to claim your premium username",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
await prisma.user.upsert({
|
||||
where: { email: userEmail },
|
||||
update: {
|
||||
username,
|
||||
password: hashedPassword,
|
||||
emailVerified: new Date(Date.now()),
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
},
|
||||
create: {
|
||||
username,
|
||||
email: userEmail,
|
||||
password: hashedPassword,
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
},
|
||||
});
|
||||
await sendEmailVerification({
|
||||
email: userEmail,
|
||||
username,
|
||||
language,
|
||||
});
|
||||
}
|
||||
|
||||
await sendEmailVerification({
|
||||
email: userEmail,
|
||||
username,
|
||||
language,
|
||||
});
|
||||
|
||||
res.status(201).json({ message: "Created user" });
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { IdentityProvider } from "@calcom/prisma/client";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
@@ -28,7 +29,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
if (!user.password) {
|
||||
if (!user.password && user.identityProvider === IdentityProvider.CAL) {
|
||||
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
|
||||
}
|
||||
|
||||
@@ -36,9 +37,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.json({ message: "Two factor disabled" });
|
||||
}
|
||||
|
||||
const isCorrectPassword = await verifyPassword(req.body.password, user.password);
|
||||
if (!isCorrectPassword) {
|
||||
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
|
||||
if (user.password && user.identityProvider === IdentityProvider.CAL) {
|
||||
const isCorrectPassword = await verifyPassword(req.body.password, user.password);
|
||||
if (!isCorrectPassword) {
|
||||
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
|
||||
}
|
||||
}
|
||||
// if user has 2fa
|
||||
if (user.twoFactorEnabled) {
|
||||
|
||||
@@ -55,7 +55,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
],
|
||||
};
|
||||
|
||||
req.statusCode = 200;
|
||||
res.statusCode = 200;
|
||||
|
||||
res.setHeader("Content-Type", "text/html");
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import type { Session } from "next-auth";
|
||||
|
||||
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
|
||||
import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
@@ -37,6 +38,9 @@ const defaultIntegrationAddHandler = async ({
|
||||
throw new Error("App is already installed");
|
||||
}
|
||||
}
|
||||
|
||||
await throwIfNotHaveAdminAccessToTeam({ teamId: teamId ?? null, userId: user.id });
|
||||
|
||||
await createCredential({ user: user, appType, slug, teamId });
|
||||
};
|
||||
|
||||
@@ -72,7 +76,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
return res.status(200);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
if (error instanceof HttpError) {
|
||||
return res.status(error.statusCode).json({ message: error.message });
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
@@ -92,6 +93,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
eventType: {
|
||||
select: {
|
||||
teamId: true,
|
||||
parentId: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
@@ -168,13 +170,20 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
uid: booking.uid,
|
||||
};
|
||||
|
||||
const teamId = await getTeamIdFromEventType({
|
||||
eventType: {
|
||||
team: { id: booking?.eventType?.teamId ?? null },
|
||||
parentId: booking?.eventType?.parentId ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
evt,
|
||||
downloadLink,
|
||||
booking: {
|
||||
userId: booking?.user?.id,
|
||||
eventTypeId: booking.eventTypeId,
|
||||
teamId: booking.eventType?.teamId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
|
||||
import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
@@ -22,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const { code, state } = req.query;
|
||||
const parsedState = stateSchema.parse(JSON.parse(state as string));
|
||||
const { teamId } = parsedState;
|
||||
|
||||
await throwIfNotHaveAdminAccessToTeam({ teamId: Number(teamId) ?? null, userId: session.user.id });
|
||||
if (code && typeof code !== "string") {
|
||||
res.status(400).json({ message: "`code` must be a string" });
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { featureFlagRouter } from "@calcom/features/flags/server/router";
|
||||
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
|
||||
|
||||
export default createNextApiHandler(featureFlagRouter);
|
||||
export default createNextApiHandler(featureFlagRouter, true, "features");
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function Login({
|
||||
.string()
|
||||
.min(1, `${t("error_required_field")}`)
|
||||
.email(`${t("enter_valid_email")}`),
|
||||
password: z.string().min(1, `${t("error_required_field")}`),
|
||||
password: !!totpEmail ? z.literal("") : z.string().min(1, `${t("error_required_field")}`),
|
||||
})
|
||||
// Passthrough other fields like totpCode
|
||||
.passthrough();
|
||||
|
||||
@@ -23,9 +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 { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
|
||||
import {
|
||||
SystemField,
|
||||
getBookingFieldsWithSystemFields,
|
||||
SMS_REMINDER_NUMBER_FIELD,
|
||||
} from "@calcom/features/bookings/lib/getBookingFields";
|
||||
@@ -440,7 +440,9 @@ export default function Success(props: SuccessProps) {
|
||||
{bookingInfo?.user && (
|
||||
<div className="mb-3">
|
||||
<div>
|
||||
<span className="mr-2">{bookingInfo.user.name}</span>
|
||||
<span data-testid={`host-name-${bookingInfo.user.name}`} className="mr-2">
|
||||
{bookingInfo.user.name}
|
||||
</span>
|
||||
<Badge variant="blue">{t("Host")}</Badge>
|
||||
</div>
|
||||
<p className="text-default">{bookingInfo.user.email}</p>
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Booker } from "@calcom/atoms";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
@@ -10,14 +11,24 @@ import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||
|
||||
export default function Type({ slug, user, booking, away, isBrandingHidden, isTeamEvent, org }: PageProps) {
|
||||
export default function Type({
|
||||
slug,
|
||||
isEmbed,
|
||||
user,
|
||||
booking,
|
||||
away,
|
||||
isBrandingHidden,
|
||||
isTeamEvent,
|
||||
org,
|
||||
}: PageProps) {
|
||||
return (
|
||||
<main className="flex h-full min-h-[100dvh] items-center justify-center">
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
<BookerSeo
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { FC } from "react";
|
||||
import { useEffect, useState, memo } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useOrgBrandingValues } from "@calcom/features/ee/organizations/hooks";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import useIntercom from "@calcom/features/ee/support/lib/intercom/useIntercom";
|
||||
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||
import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
|
||||
@@ -16,6 +16,7 @@ import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
|
||||
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
@@ -102,7 +103,7 @@ const querySchema = z.object({
|
||||
|
||||
const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
|
||||
const { eventTypeGroups } = props;
|
||||
const orgBranding = useOrgBrandingValues();
|
||||
const orgBranding = useOrgBranding();
|
||||
const tabs = eventTypeGroups.map((item) => ({
|
||||
name: item.profile.name ?? "",
|
||||
href: item.teamId ? `/event-types?teamId=${item.teamId}` : "/event-types",
|
||||
@@ -200,7 +201,7 @@ const MemoizedItem = memo(Item);
|
||||
export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeListProps): JSX.Element => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const orgBranding = useOrgBrandingValues();
|
||||
const orgBranding = useOrgBranding();
|
||||
const [parent] = useAutoAnimate<HTMLUListElement>();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0);
|
||||
@@ -513,7 +514,8 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
type="button"
|
||||
StartIcon={Code}
|
||||
className="w-full rounded-none"
|
||||
embedUrl={encodeURIComponent(embedLink)}>
|
||||
embedUrl={encodeURIComponent(embedLink)}
|
||||
eventId={type.id}>
|
||||
{t("embed")}
|
||||
</EmbedButton>
|
||||
</DropdownMenuItem>
|
||||
@@ -699,7 +701,7 @@ const EventTypeListHeading = ({
|
||||
}: EventTypeListHeadingProps): JSX.Element => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const orgBranding = useOrgBrandingValues();
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
const publishTeamMutation = trpc.viewer.teams.publish.useMutation({
|
||||
onSuccess(data) {
|
||||
@@ -709,7 +711,7 @@ const EventTypeListHeading = ({
|
||||
showToast(error.message, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const bookerUrl = useBookerUrl();
|
||||
return (
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<Avatar
|
||||
@@ -740,9 +742,7 @@ const EventTypeListHeading = ({
|
||||
)}
|
||||
{profile?.slug && (
|
||||
<Link href={`${CAL_URL}/${profile.slug}`} className="text-subtle block text-xs">
|
||||
{orgBranding
|
||||
? `${orgBranding.fullDomain.replace("https://", "").replace("http://", "")}${profile.slug}`
|
||||
: `${CAL_URL?.replace("https://", "").replace("http://", "")}/${profile.slug}`}
|
||||
{`${bookerUrl.replace("https://", "").replace("http://", "")}/${profile.slug}`}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
@@ -808,7 +808,7 @@ const Actions = () => {
|
||||
|
||||
const SetupProfileBanner = ({ closeAction }: { closeAction: () => void }) => {
|
||||
const { t } = useLocale();
|
||||
const orgBranding = useOrgBrandingValues();
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
return (
|
||||
<Alert
|
||||
@@ -839,7 +839,7 @@ const Main = () => {
|
||||
const router = useRouter();
|
||||
const filters = getTeamsFiltersFromQuery(router.query);
|
||||
|
||||
const orgBranding = useOrgBrandingValues();
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
// TODO: Maybe useSuspenseQuery to focus on success case only? Remember that it would crash the page when there is an error in query. Also, it won't support skeleton
|
||||
const res = trpc.viewer.eventTypes.getByViewer.useQuery(filters && { filters });
|
||||
@@ -905,7 +905,7 @@ const EventTypesPage = () => {
|
||||
const { query } = router;
|
||||
const { data: user } = useMeQuery();
|
||||
const [showProfileBanner, setShowProfileBanner] = useState(false);
|
||||
const orgBranding = useOrgBrandingValues();
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
function closeBanner() {
|
||||
setShowProfileBanner(false);
|
||||
|
||||
@@ -42,6 +42,7 @@ const stepTransform = (step: (typeof steps)[number]) => {
|
||||
|
||||
const stepRouteSchema = z.object({
|
||||
step: z.array(z.enum(steps)).default([INITIAL_STEP]),
|
||||
from: z.string().optional(),
|
||||
});
|
||||
|
||||
// TODO: Refactor how steps work to be contained in one array/object. Currently we have steps,initalsteps,headers etc. These can all be in one place
|
||||
@@ -52,6 +53,7 @@ const OnboardingPage = () => {
|
||||
|
||||
const result = stepRouteSchema.safeParse(router.query);
|
||||
const currentStep = result.success ? result.data.step[0] : INITIAL_STEP;
|
||||
const from = result.success ? result.data.from : "";
|
||||
|
||||
const headers = [
|
||||
{
|
||||
@@ -141,7 +143,9 @@ const OnboardingPage = () => {
|
||||
</div>
|
||||
<StepCard>
|
||||
<Suspense fallback={<Loader />}>
|
||||
{currentStep === "user-settings" && <UserSettings nextStep={() => goToIndex(1)} />}
|
||||
{currentStep === "user-settings" && (
|
||||
<UserSettings nextStep={() => goToIndex(1)} hideUsername={from === "signup"} />
|
||||
)}
|
||||
{currentStep === "connected-calendar" && <ConnectedCalendars nextStep={() => goToIndex(2)} />}
|
||||
|
||||
{currentStep === "connected-video" && <ConnectedVideoStep nextStep={() => goToIndex(3)} />}
|
||||
|
||||
@@ -34,14 +34,15 @@ const TwoFactorAuthView = () => {
|
||||
if (isLoading) return <SkeletonLoader />;
|
||||
|
||||
const isCalProvider = user?.identityProvider === "CAL";
|
||||
const canSetupTwoFactor = !isCalProvider && !user?.twoFactorEnabled;
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("2fa")} description={t("set_up_two_factor_authentication")} />
|
||||
{!isCalProvider && <Alert severity="neutral" message={t("2fa_disabled")} />}
|
||||
{canSetupTwoFactor && <Alert severity="neutral" message={t("2fa_disabled")} />}
|
||||
<div className="mt-6 flex items-start space-x-4">
|
||||
<Switch
|
||||
data-testid="two-factor-switch"
|
||||
disabled={!isCalProvider}
|
||||
disabled={canSetupTwoFactor}
|
||||
checked={user?.twoFactorEnabled}
|
||||
onCheckedChange={() =>
|
||||
user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true)
|
||||
@@ -72,6 +73,7 @@ const TwoFactorAuthView = () => {
|
||||
|
||||
<DisableTwoFactorModal
|
||||
open={disableModalOpen}
|
||||
disablePassword={!isCalProvider}
|
||||
onOpenChange={() => setDisableModalOpen(!disableModalOpen)}
|
||||
onDisable={() => {
|
||||
setDisableModalOpen(false);
|
||||
|
||||
+19
-10
@@ -1,3 +1,4 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -23,13 +24,18 @@ import PageWrapper from "@components/PageWrapper";
|
||||
import { IS_GOOGLE_LOGIN_ENABLED } from "../server/lib/constants";
|
||||
import { ssrInit } from "../server/lib/ssr";
|
||||
|
||||
type FormValues = {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
apiError: string;
|
||||
token?: string;
|
||||
};
|
||||
const signupSchema = z.object({
|
||||
username: z.string().refine((value) => !value.includes("+"), {
|
||||
message: "String should not contain a plus symbol (+).",
|
||||
}),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(7),
|
||||
language: z.string().optional(),
|
||||
token: z.string().optional(),
|
||||
apiError: z.string().optional(), // Needed to display API errors doesnt get passed to the API
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof signupSchema>;
|
||||
|
||||
type SignupProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
@@ -39,6 +45,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
|
||||
const flags = useFlagMap();
|
||||
const telemetry = useTelemetry();
|
||||
const methods = useForm<FormValues>({
|
||||
resolver: zodResolver(signupSchema),
|
||||
defaultValues: prepopulateFormValues,
|
||||
});
|
||||
const {
|
||||
@@ -71,9 +78,11 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
|
||||
const verifyOrGettingStarted = flags["email-verification"] ? "auth/verify-email" : "getting-started";
|
||||
await signIn<"credentials">("credentials", {
|
||||
...data,
|
||||
callbackUrl: router.query.callbackUrl
|
||||
? `${WEBAPP_URL}/${router.query.callbackUrl}`
|
||||
: `${WEBAPP_URL}/${verifyOrGettingStarted}`,
|
||||
callbackUrl: `${
|
||||
router.query.callbackUrl
|
||||
? `${WEBAPP_URL}/${router.query.callbackUrl}`
|
||||
: `${WEBAPP_URL}/${verifyOrGettingStarted}`
|
||||
}?from=signup`,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
|
||||
@@ -130,7 +130,9 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
|
||||
<div className="ms-3 inline-block truncate">
|
||||
<span className="text-default text-sm font-bold">{ch.name}</span>
|
||||
<span className="text-subtle block text-xs">
|
||||
{t("number_member", { count: ch.members.length })}
|
||||
{t("number_member", {
|
||||
count: ch.members.filter((mem) => mem.user.username !== null).length,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,11 +140,13 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
|
||||
className="mr-6"
|
||||
size="sm"
|
||||
truncateAfter={4}
|
||||
items={ch.members.map(({ user: member }) => ({
|
||||
alt: member.name || "",
|
||||
image: `/${member.username}/avatar.png`,
|
||||
title: member.name || "",
|
||||
}))}
|
||||
items={ch.members
|
||||
.filter((mem) => mem.user.username !== null)
|
||||
.map(({ user: member }) => ({
|
||||
alt: member.name || "",
|
||||
image: `/${member.username}/avatar.png`,
|
||||
title: member.name || "",
|
||||
}))}
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@@ -2,24 +2,24 @@ import type { GetServerSidePropsContext } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Booker } from "@calcom/atoms";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||
|
||||
export default function Type({ slug, user, booking, away, isBrandingHidden, org }: PageProps) {
|
||||
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
|
||||
export default function Type({ slug, user, booking, away, isEmbed, isBrandingHidden, org }: PageProps) {
|
||||
return (
|
||||
<main className={classNames("flex h-full items-center justify-center", !isEmbed && "min-h-[100dvh]")}>
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
<BookerSeo
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
|
||||
@@ -23,7 +23,7 @@ test.describe("Change Password Test", () => {
|
||||
|
||||
await page.locator("text=Update").click();
|
||||
|
||||
const toast = await page.waitForSelector("div[class*='data-testid-toast-success']");
|
||||
const toast = await page.waitForSelector('[data-testid="toast-success"]');
|
||||
|
||||
expect(toast).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ test.describe("Change Theme Test", () => {
|
||||
//Click the update button
|
||||
await page.click('[data-testid="update-theme-btn"]');
|
||||
//Wait for the toast to appear
|
||||
const toast = await page.waitForSelector("div[class*='data-testid-toast-success']");
|
||||
const toast = await page.waitForSelector('[data-testid="toast-success"]');
|
||||
expect(toast).toBeTruthy();
|
||||
//Go to the profile page and check if the theme is dark
|
||||
await page.goto(`/${pro.username}`);
|
||||
@@ -35,7 +35,7 @@ test.describe("Change Theme Test", () => {
|
||||
//Click the update theme button
|
||||
await page.click('[data-testid="update-theme-btn"]');
|
||||
//Wait for the toast to appear
|
||||
const toast = await page.waitForSelector("div[class*='data-testid-toast-success']");
|
||||
const toast = await page.waitForSelector('[data-testid="toast-success"]');
|
||||
expect(toast).toBeTruthy();
|
||||
//Go to the profile page and check if the theme is light
|
||||
await page.goto(`/${pro.username}`);
|
||||
|
||||
@@ -97,7 +97,7 @@ test.describe("Event Types tests", () => {
|
||||
return !!url.pathname.match(/\/event-types\/.+/);
|
||||
});
|
||||
await page.locator("[data-testid=update-eventtype]").click();
|
||||
const toast = await page.waitForSelector("div[class*='data-testid-toast-success']");
|
||||
const toast = await page.waitForSelector('[data-testid="toast-success"]');
|
||||
expect(toast).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
@@ -229,6 +229,7 @@ export const createUsersFixture = (page: Page, workerInfo: WorkerInfo) => {
|
||||
id: _user.id,
|
||||
},
|
||||
},
|
||||
schedulingType: "COLLECTIVE",
|
||||
title: "Team Event - 30min",
|
||||
slug: "team-event-30min",
|
||||
length: 30,
|
||||
|
||||
@@ -44,7 +44,8 @@ test.describe("2FA Tests", async () => {
|
||||
secret: secret!,
|
||||
});
|
||||
|
||||
await expect(page.locator(`[data-testid=two-factor-switch][data-state="checked"]`)).toBeVisible();
|
||||
await page.waitForSelector(`[data-testid=two-factor-switch]`);
|
||||
await expect(page.locator(`[data-testid=two-factor-switch]`).isChecked()).toBeTruthy();
|
||||
|
||||
return user;
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { uuid } from "short-uuid";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { createHttpServer, waitFor, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
|
||||
@@ -40,6 +41,101 @@ test.describe("Manage Booking Questions", () => {
|
||||
|
||||
await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver);
|
||||
});
|
||||
|
||||
test("Split 'Full name' into 'First name' and 'Last name'", async ({
|
||||
page,
|
||||
users,
|
||||
context,
|
||||
}, testInfo) => {
|
||||
// Considering there are many steps in it, it would need more than default test timeout
|
||||
test.setTimeout(testInfo.timeout * 3);
|
||||
const user = await createAndLoginUserWithEventTypes({ page, users });
|
||||
const webhookReceiver = await addWebhook(user);
|
||||
await test.step("Go to first EventType Page ", async () => {
|
||||
const $eventTypes = page.locator("[data-testid=event-types] > li a");
|
||||
const firstEventTypeElement = $eventTypes.first();
|
||||
|
||||
await firstEventTypeElement.click();
|
||||
});
|
||||
|
||||
await test.step("Open the 'Name' field dialog", async () => {
|
||||
await page.click('[href$="tabName=advanced"]');
|
||||
await page.locator('[data-testid="field-name"] [data-testid="edit-field-action"]').click();
|
||||
});
|
||||
|
||||
await test.step("Toggle on the variant toggle and save Event Type", async () => {
|
||||
await page.click('[data-testid="variant-toggle"]');
|
||||
await page.click("[data-testid=field-add-save]");
|
||||
await saveEventType(page);
|
||||
});
|
||||
|
||||
await test.step("Book a time slot with firstName and lastName provided separately", async () => {
|
||||
await doOnFreshPreview(page, context, async (page) => {
|
||||
await expectSystemFieldsToBeThereOnBookingPage({ page, isFirstAndLastNameVariant: true });
|
||||
await bookTimeSlot({
|
||||
page,
|
||||
name: { firstName: "John", lastName: "Doe" },
|
||||
email: "booker@example.com",
|
||||
});
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
expect(await page.locator('[data-testid="attendee-name-John Doe"]').nth(0).textContent()).toBe(
|
||||
"John Doe"
|
||||
);
|
||||
await expectWebhookToBeCalled(webhookReceiver, {
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
|
||||
payload: {
|
||||
attendees: [
|
||||
{
|
||||
// It would have full Name only
|
||||
name: "John Doe",
|
||||
email: "booker@example.com",
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
name: {
|
||||
label: "your_name",
|
||||
value: {
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
},
|
||||
},
|
||||
email: {
|
||||
label: "email_address",
|
||||
value: "booker@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Verify that we can prefill name and other fields correctly", async () => {
|
||||
await doOnFreshPreview(page, context, async (page) => {
|
||||
const url = page.url();
|
||||
const prefillUrl = new URL(url);
|
||||
prefillUrl.searchParams.append("name", "John Johny Janardan");
|
||||
prefillUrl.searchParams.append("email", "john@example.com");
|
||||
prefillUrl.searchParams.append("guests", "guest1@example.com");
|
||||
prefillUrl.searchParams.append("guests", "guest2@example.com");
|
||||
prefillUrl.searchParams.append("notes", "This is an additional note");
|
||||
await page.goto(prefillUrl.toString());
|
||||
await bookTimeSlot({ page, skipSubmission: true });
|
||||
await expectSystemFieldsToBeThereOnBookingPage({
|
||||
page,
|
||||
isFirstAndLastNameVariant: true,
|
||||
values: {
|
||||
name: {
|
||||
firstName: "John",
|
||||
lastName: "Johny Janardan",
|
||||
},
|
||||
email: "john@example.com",
|
||||
guests: ["guest1@example.com", "guest2@example.com"],
|
||||
notes: "This is an additional note",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("For Team EventType", () => {
|
||||
@@ -91,12 +187,23 @@ async function runTestStepsCommonForTeamAndUserEventType(
|
||||
}
|
||||
) {
|
||||
await page.click('[href$="tabName=advanced"]');
|
||||
|
||||
await test.step("Check that all the system questions are shown in the list", async () => {
|
||||
await page.locator("[data-testid=field-name]").isVisible();
|
||||
await page.locator("[data-testid=field-email]").isVisible();
|
||||
await page.locator("[data-testid=field-notes]").isVisible();
|
||||
await page.locator("[data-testid=field-guests]").isVisible();
|
||||
await page.locator("[data-testid=field-rescheduleReason]").isVisible();
|
||||
// It is conditional
|
||||
// await page.locator("data-testid=field-location").isVisible();
|
||||
});
|
||||
|
||||
await test.step("Add Question and see that it's shown on Booking Page at appropriate position", async () => {
|
||||
await addQuestionAndSave({
|
||||
page,
|
||||
question: {
|
||||
name: "how_are_you",
|
||||
type: "Name",
|
||||
type: "Address",
|
||||
label: "How are you?",
|
||||
placeholder: "I'm fine, thanks",
|
||||
required: true,
|
||||
@@ -104,13 +211,13 @@ async function runTestStepsCommonForTeamAndUserEventType(
|
||||
});
|
||||
|
||||
await doOnFreshPreview(page, context, async (page) => {
|
||||
const allFieldsLocator = await expectSystemFieldsToBeThere(page);
|
||||
const allFieldsLocator = await expectSystemFieldsToBeThereOnBookingPage({ page });
|
||||
const userFieldLocator = allFieldsLocator.nth(5);
|
||||
|
||||
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[type=text]")).toBeVisible();
|
||||
await expect(userFieldLocator.locator("input")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -190,8 +297,6 @@ async function runTestStepsCommonForTeamAndUserEventType(
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload.location).toBe("integrations:daily");
|
||||
|
||||
expect(payload.attendees[0]).toMatchObject({
|
||||
name: "Booker",
|
||||
email: "booker@example.com",
|
||||
@@ -217,7 +322,24 @@ async function runTestStepsCommonForTeamAndUserEventType(
|
||||
});
|
||||
}
|
||||
|
||||
async function expectSystemFieldsToBeThere(page: Page) {
|
||||
async function expectSystemFieldsToBeThereOnBookingPage({
|
||||
page,
|
||||
isFirstAndLastNameVariant,
|
||||
values,
|
||||
}: {
|
||||
page: Page;
|
||||
isFirstAndLastNameVariant?: boolean;
|
||||
values?: Partial<{
|
||||
name: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
fullName?: string;
|
||||
};
|
||||
email: string;
|
||||
notes: string;
|
||||
guests: string[];
|
||||
}>;
|
||||
}) {
|
||||
const allFieldsLocator = page.locator("[data-fob-field-name]:not(.hidden)");
|
||||
const nameLocator = allFieldsLocator.nth(0);
|
||||
const emailLocator = allFieldsLocator.nth(1);
|
||||
@@ -226,11 +348,44 @@ async function expectSystemFieldsToBeThere(page: Page) {
|
||||
const additionalNotes = allFieldsLocator.nth(3);
|
||||
const guestsLocator = allFieldsLocator.nth(4);
|
||||
|
||||
await expect(nameLocator.locator('[name="name"]')).toBeVisible();
|
||||
await expect(emailLocator.locator('[name="email"]')).toBeVisible();
|
||||
if (isFirstAndLastNameVariant) {
|
||||
if (values?.name) {
|
||||
await expect(nameLocator.locator('[name="firstName"]')).toHaveValue(values?.name?.firstName || "");
|
||||
await expect(nameLocator.locator('[name="lastName"]')).toHaveValue(values?.name?.lastName || "");
|
||||
expect(await nameLocator.locator(".testid-firstName > label").innerText()).toContain("*");
|
||||
} else {
|
||||
await expect(nameLocator.locator('[name="firstName"]')).toBeVisible();
|
||||
await expect(nameLocator.locator('[name="lastName"]')).toBeVisible();
|
||||
}
|
||||
} else {
|
||||
if (values?.name) {
|
||||
await expect(nameLocator.locator('[name="name"]')).toHaveValue(values?.name?.fullName || "");
|
||||
}
|
||||
await expect(nameLocator.locator('[name="name"]')).toBeVisible();
|
||||
expect(await nameLocator.locator("label").innerText()).toContain("*");
|
||||
}
|
||||
|
||||
await expect(additionalNotes.locator('[name="notes"]')).toBeVisible();
|
||||
await expect(guestsLocator.locator("button")).toBeVisible();
|
||||
if (values?.email) {
|
||||
await expect(emailLocator.locator('[name="email"]')).toHaveValue(values?.email || "");
|
||||
} else {
|
||||
await expect(emailLocator.locator('[name="email"]')).toBeVisible();
|
||||
}
|
||||
|
||||
if (values?.notes) {
|
||||
await expect(additionalNotes.locator('[name="notes"]')).toHaveValue(values?.notes);
|
||||
} else {
|
||||
await expect(additionalNotes.locator('[name="notes"]')).toBeVisible();
|
||||
}
|
||||
|
||||
if (values?.guests) {
|
||||
const allGuestsLocators = guestsLocator.locator('[type="email"]');
|
||||
for (let i = 0; i < values.guests.length; i++) {
|
||||
await expect(allGuestsLocators.nth(i)).toHaveValue(values.guests[i] || "");
|
||||
}
|
||||
await expect(guestsLocator.locator("[data-testid='add-another-guest']")).toBeVisible();
|
||||
} else {
|
||||
await expect(guestsLocator.locator("[data-testid='add-guests']")).toBeVisible();
|
||||
}
|
||||
return allFieldsLocator;
|
||||
}
|
||||
|
||||
@@ -238,11 +393,33 @@ async function expectSystemFieldsToBeThere(page: Page) {
|
||||
// Verify webhook is sent with the correct data, DB is correct (including metadata)
|
||||
|
||||
//TODO: Verify that prefill works
|
||||
async function bookTimeSlot({ page, name, email }: { page: Page; name: string; email: string }) {
|
||||
// --- fill form
|
||||
await page.fill('[name="name"]', name);
|
||||
await page.fill('[name="email"]', email);
|
||||
await page.press('[name="email"]', "Enter");
|
||||
async function bookTimeSlot({
|
||||
page,
|
||||
name,
|
||||
email,
|
||||
skipSubmission = false,
|
||||
}: {
|
||||
page: Page;
|
||||
name?: string | { firstName: string; lastName?: string };
|
||||
email?: string;
|
||||
skipSubmission?: boolean;
|
||||
}) {
|
||||
if (name) {
|
||||
if (typeof name === "string") {
|
||||
await page.fill('[name="name"]', name);
|
||||
} else {
|
||||
await page.fill('[name="firstName"]', name.firstName);
|
||||
if (name.lastName) {
|
||||
await page.fill('[name="lastName"]', name.lastName);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (email) {
|
||||
await page.fill('[name="email"]', email);
|
||||
}
|
||||
if (!skipSubmission) {
|
||||
await page.press('[name="email"]', "Enter");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -424,3 +601,22 @@ async function addWebhook(
|
||||
|
||||
return webhookReceiver;
|
||||
}
|
||||
|
||||
async function expectWebhookToBeCalled(
|
||||
webhookReceiver: Awaited<ReturnType<typeof addWebhook>>,
|
||||
expectedBody: {
|
||||
triggerEvent: WebhookTriggerEvents;
|
||||
payload: Omit<Partial<CalendarEvent>, "attendees"> & {
|
||||
attendees: Partial<CalendarEvent["attendees"][number]>[];
|
||||
};
|
||||
}
|
||||
) {
|
||||
await waitFor(() => {
|
||||
expect(webhookReceiver.requestList.length).toBe(1);
|
||||
});
|
||||
const [request] = webhookReceiver.requestList;
|
||||
|
||||
const body = request.body;
|
||||
|
||||
expect(body).toMatchObject(expectedBody);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { bookTimeSlot, selectFirstAvailableTimeSlotNextMonth, testName } from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
@@ -62,4 +64,86 @@ test.describe("Teams", () => {
|
||||
// await expect(page.locator('[data-testid="empty-screen"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
test("Can create a booking for Collective EventType", async ({ page, users }) => {
|
||||
const ownerObj = { username: "pro-user", name: "pro-user" };
|
||||
const teamMate1Obj = { username: "teammate-1", name: "teammate-1" };
|
||||
const teamMate2Obj = { username: "teammate-2", name: "teammate-2" };
|
||||
|
||||
const owner = await users.create(ownerObj, { hasTeam: true });
|
||||
const teamMate1 = await users.create(teamMate1Obj);
|
||||
const teamMate2 = await users.create(teamMate2Obj);
|
||||
teamMate1.username, teamMate2.username;
|
||||
// TODO: Create a fixture and follow DRY
|
||||
const { team } = await prisma.membership.findFirstOrThrow({
|
||||
where: { userId: owner.id },
|
||||
include: { team: true },
|
||||
});
|
||||
|
||||
// Add teamMate1 to the team
|
||||
await prisma.membership.create({
|
||||
data: {
|
||||
teamId: team.id,
|
||||
userId: teamMate1.id,
|
||||
role: MembershipRole.MEMBER,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Add teamMate2 to the team
|
||||
await prisma.membership.create({
|
||||
data: {
|
||||
teamId: team.id,
|
||||
userId: teamMate2.id,
|
||||
role: MembershipRole.MEMBER,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
// No need to remove membership row manually as it will be deleted with the user, at the end of the test.
|
||||
|
||||
const { id: eventTypeId } = await prisma.eventType.findFirstOrThrow({
|
||||
where: { userId: owner.id, teamId: team.id },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
// Assign owner and teammates to the eventtype host list
|
||||
await prisma.host.create({
|
||||
data: {
|
||||
userId: owner.id,
|
||||
eventTypeId,
|
||||
isFixed: true,
|
||||
},
|
||||
});
|
||||
await prisma.host.create({
|
||||
data: {
|
||||
userId: teamMate1.id,
|
||||
eventTypeId,
|
||||
isFixed: true,
|
||||
},
|
||||
});
|
||||
await prisma.host.create({
|
||||
data: {
|
||||
userId: teamMate2.id,
|
||||
eventTypeId,
|
||||
isFixed: true,
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto(`/team/${team.slug}/team-event-30min`);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page);
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
|
||||
|
||||
// The first user added to the host list will be the host name
|
||||
await expect(page.locator(`[data-testid="host-name-${ownerObj.name}"]`)).toHaveText(ownerObj.name);
|
||||
|
||||
// The remaining hosts will be shown as the attendees
|
||||
await expect(page.locator(`[data-testid="attendee-name-${teamMate1Obj.name}"]`)).toHaveText(
|
||||
teamMate1Obj.name
|
||||
);
|
||||
await expect(page.locator(`[data-testid="attendee-name-${teamMate2Obj.name}"]`)).toHaveText(
|
||||
teamMate2Obj.name
|
||||
);
|
||||
// TODO: Assert whether the user received an email
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { WorkflowMethods } from "@calcom/prisma/enums";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { selectFirstAvailableTimeSlotNextMonth, todo } from "./lib/testUtils";
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
test.describe("Workflow tests", () => {
|
||||
test.describe("User Workflows", () => {
|
||||
test("Create default reminder workflow & trigger when event type is booked", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
const [eventType] = user.eventTypes;
|
||||
await user.login();
|
||||
await page.goto(`/workflows`);
|
||||
|
||||
await page.click('[data-testid="create-button"]');
|
||||
|
||||
// select first event type
|
||||
await page.getByText("Select...").click();
|
||||
await page.getByText(eventType.title, { exact: true }).click();
|
||||
|
||||
// name workflow
|
||||
await page.fill('[data-testid="workflow-name"]', "Test workflow");
|
||||
|
||||
// save workflow
|
||||
await page.click('[data-testid="save-workflow"]');
|
||||
|
||||
// check if workflow is saved
|
||||
await expect(page.locator('[data-testid="workflow-list"] > li')).toHaveCount(1);
|
||||
|
||||
// book event type
|
||||
await page.goto(`/${user.username}/${eventType.slug}`);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
await page.fill('[name="name"]', "Test");
|
||||
await page.fill('[name="email"]', "test@example.com");
|
||||
await page.press('[name="email"]', "Enter");
|
||||
|
||||
// Make sure booking is completed
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
const booking = await prisma.booking.findFirst({
|
||||
where: {
|
||||
eventTypeId: eventType.id,
|
||||
},
|
||||
});
|
||||
|
||||
// check if workflow triggered
|
||||
const workflowReminders = await prisma.workflowReminder.findMany({
|
||||
where: {
|
||||
bookingUid: booking?.uid ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
expect(workflowReminders).toHaveLength(1);
|
||||
|
||||
const scheduledDate = dayjs(booking?.startTime).subtract(1, "day").toDate();
|
||||
|
||||
expect(workflowReminders[0].method).toBe(WorkflowMethods.EMAIL);
|
||||
expect(workflowReminders[0].scheduledDate.toISOString()).toBe(scheduledDate.toISOString());
|
||||
});
|
||||
|
||||
// add all other actions to this workflow and test if they triggered
|
||||
// cancel booking and test if workflow reminders are deleted
|
||||
// test all other triggers
|
||||
});
|
||||
|
||||
test.describe("Team Workflows", () => {
|
||||
todo("Admin can create and update team workflow");
|
||||
todo("Members can not create and update team workflows");
|
||||
});
|
||||
});
|
||||
@@ -1173,6 +1173,7 @@
|
||||
"event_time_variable": "وقت الحدث",
|
||||
"timezone_variable": "المنطقة الزمنية",
|
||||
"location_variable": "الموقع",
|
||||
"additional_notes_variable": "ملاحظات إضافية",
|
||||
"organizer_name_variable": "اسم المنظم",
|
||||
"app_upgrade_description": "لاستخدام هذه الميزة، تحتاج إلى الترقية إلى حساب Pro.",
|
||||
"invalid_number": "رقم الهاتف غير صالح",
|
||||
|
||||
@@ -1178,6 +1178,7 @@
|
||||
"event_time_variable": "Čas události",
|
||||
"timezone_variable": "Časová zóna",
|
||||
"location_variable": "Místo",
|
||||
"additional_notes_variable": "Doplňující poznámky",
|
||||
"organizer_name_variable": "Jméno organizátora",
|
||||
"app_upgrade_description": "Pokud chcete použít tuto funkci, musíte provést aktualizaci na účet Pro.",
|
||||
"invalid_number": "Neplatné telefonní číslo",
|
||||
|
||||
@@ -1056,6 +1056,7 @@
|
||||
"event_time_variable": "Tidspunkt for begivenhed",
|
||||
"timezone_variable": "Tidszone",
|
||||
"location_variable": "Placering",
|
||||
"additional_notes_variable": "Yderligere bemærkninger",
|
||||
"app_upgrade_description": "For at bruge denne funktion skal du opgradere til en Pro-konto.",
|
||||
"invalid_number": "Ugyldigt telefonnummer",
|
||||
"navigate": "Navigér",
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
"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_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.",
|
||||
"verify_email_email_body": "Bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie unten auf die Schaltfläche klicken.",
|
||||
"verify_email_email_link_text": "Hier ist der Link, falls Sie lieber keine Schaltflächen anklicken möchten:",
|
||||
"email_sent": "E-Mail erfolgreich gesendet",
|
||||
"event_declined_subject": "Abgelehnt: {{title}} am {{date}}",
|
||||
"event_cancelled_subject": "Storniert: {{title}} am {{date}}",
|
||||
"event_cancelled_subject": "Abgesagt: {{title}} am {{date}}",
|
||||
"event_request_declined": "Ihre Event-Anfrage wurde abgelehnt",
|
||||
"event_request_declined_recurring": "Ihre wiederkehrende Terminanfrage wurde abgelehnt",
|
||||
"event_request_cancelled": "Ihr geplanter Termin wurde abgesagt",
|
||||
@@ -84,10 +85,10 @@
|
||||
"booking_submitted_subject": "Buchung eingereicht: {{title}} am {{date}}",
|
||||
"download_recording_subject": "Aufnahme herunterladen: {{title}} am {{date}}",
|
||||
"download_your_recording": "Ihre Aufnahme herunterladen",
|
||||
"your_meeting_has_been_booked": "Dein Termin wurde gebucht",
|
||||
"your_meeting_has_been_booked": "Ihr Termin wurde gebucht",
|
||||
"event_type_has_been_rescheduled_on_time_date": "Ihr {{title}} wurde auf den {{date}} verschoben.",
|
||||
"event_has_been_rescheduled": "Ihr Termin wurde verschoben.",
|
||||
"request_reschedule_subtitle": "{{organizer}} hat die Buchung storniert und Sie gebeten, eine andere Zeit auszuwählen.",
|
||||
"request_reschedule_subtitle": "{{organizer}} hat die Buchung abgesagt und Sie gebeten, eine andere Zeit auszuwählen.",
|
||||
"request_reschedule_title_organizer": "Sie haben {{attendee}} zur Neuplanung aufgefordert",
|
||||
"request_reschedule_subtitle_organizer": "Sie haben die Buchung storniert und {{attendee}} muss eine neue Buchungszeit mit Ihnen vereinbaren.",
|
||||
"rescheduled_event_type_subject": "Anfrage für Neuplanung gesendet: {{eventType}} mit {{name}} am {{date}}",
|
||||
@@ -243,6 +244,8 @@
|
||||
"all": "Alle Apps",
|
||||
"yours": "Ihr Konto",
|
||||
"available_apps": "Verfügbare Apps",
|
||||
"available_apps_lower_case": "Verfügbare Apps",
|
||||
"available_apps_desc": "Sie haben keine Apps installiert. Sehen Sie sich beliebte Apps unten an und entdecken Sie noch mehr in unserem <1>App Store</1>",
|
||||
"check_email_reset_password": "Überprüfen Sie Ihre E-Mail. Wir haben Ihnen einen Link zum Zurücksetzen Ihres Passworts gesendet.",
|
||||
"finish": "Fertig",
|
||||
"organization_general_description": "Einstellungen für die Sprache und Zeitzone Ihres Teams verwalten",
|
||||
@@ -328,7 +331,7 @@
|
||||
"error_end_time_next_day": "Die Endzeit darf nicht größer als 24 Stunden sein",
|
||||
"back_to_bookings": "Zurück zu den Buchungen",
|
||||
"free_to_pick_another_event_type": "Sie können jederzeit einen anderen Termin wählen.",
|
||||
"cancelled": "Storniert",
|
||||
"cancelled": "Abgesagt",
|
||||
"cancellation_successful": "Stornierung erfolgreich",
|
||||
"really_cancel_booking": "Möchten Sie wirklich stornieren?",
|
||||
"cannot_cancel_booking": "Sie können diese Buchung nicht stornieren",
|
||||
@@ -342,7 +345,7 @@
|
||||
"error_event_type_unauthorized_update": "Sie können diesen Termin nicht bearbeiten",
|
||||
"error_workflow_unauthorized_create": "Sie können diesen Workflow nicht erstellen",
|
||||
"error_schedule_unauthorized_create": "Sie können diesen Verfügbarkeitsplan nicht erstellen",
|
||||
"booking_already_cancelled": "Diese Buchung wurde bereits storniert",
|
||||
"booking_already_cancelled": "Diese Buchung wurde bereits abgesagt",
|
||||
"booking_already_accepted_rejected": "Diese Buchung wurde bereits angenommen oder abgelehnt",
|
||||
"go_back_home": "Zurück zum Anfang",
|
||||
"or_go_back_home": "Oder navigieren Sie zurück",
|
||||
@@ -357,7 +360,7 @@
|
||||
"upcoming_bookings": "Sobald jemand eine Zeit bei Ihnen bucht können Sie das hier sehen.",
|
||||
"recurring_bookings": "Sobald jemand einen wiederkehrenden Termin mit Ihnen bucht wird er hier erscheinen.",
|
||||
"past_bookings": "Ihre früheren Buchungen werden hier angezeigt.",
|
||||
"cancelled_bookings": "Ihre stornierten Buchungen werden hier angezeigt.",
|
||||
"cancelled_bookings": "Ihre abgesagten Buchungen werden hier angezeigt.",
|
||||
"unconfirmed_bookings": "Ihre unbestätigten Buchungen werden hier angezeigt.",
|
||||
"unconfirmed_bookings_tooltip": "Unbestätigte Buchungen",
|
||||
"on": "auf",
|
||||
@@ -398,7 +401,7 @@
|
||||
"browse_api_documentation": "Durchsuchen Sie unsere API Dokumentation",
|
||||
"leverage_our_api": "Nutzen Sie unsere API für die volle Kontrolle und Anpassbarkeit.",
|
||||
"create_webhook": "Webhook erstellen",
|
||||
"booking_cancelled": "Buchung storniert",
|
||||
"booking_cancelled": "Buchung abgesagt",
|
||||
"booking_rescheduled": "Termin verlegt",
|
||||
"recording_ready": "Download-Link der Aufnahme ist nun verfügbar",
|
||||
"booking_created": "Termin erstellt",
|
||||
@@ -406,6 +409,7 @@
|
||||
"booking_requested": "Buchung angefragt",
|
||||
"meeting_ended": "Meeting beendet",
|
||||
"form_submitted": "Formular gesendet",
|
||||
"booking_paid": "Buchung bezahlt",
|
||||
"event_triggers": "Event-Auslöser",
|
||||
"subscriber_url": "Subscriber-URL",
|
||||
"create_new_webhook": "Einen neuen Webhook erstellen",
|
||||
@@ -1178,6 +1182,7 @@
|
||||
"event_time_variable": "Event Zeit",
|
||||
"timezone_variable": "Zeitzone",
|
||||
"location_variable": "Ort",
|
||||
"additional_notes_variable": "Zusätzliche Notizen",
|
||||
"organizer_name_variable": "Organisator Name",
|
||||
"app_upgrade_description": "Um diese Funktion nutzen zu können, müssen Sie ein Upgrade auf einen Pro-Account durchführen.",
|
||||
"invalid_number": "Ungültige Telefonnummer",
|
||||
@@ -1864,6 +1869,9 @@
|
||||
"open_dialog_with_element_click": "Öffnet Ihren Cal-Dialog, wenn jemand ein Bedienelement anklickt.",
|
||||
"need_help_embedding": "Brauchen Sie Hilfe? Sehen Sie sich unsere Anleitungen zum Einbetten von Cal auf Wix, Squarespace, oder WordPress an, schauen Sie sich unsere häufig gestellten Fragen an oder erkunden Sie erweiterte Einbettungsoptionen.",
|
||||
"book_my_cal": "Mich per Cal buchen",
|
||||
"first_name": "Vorname",
|
||||
"last_name": "Nachname",
|
||||
"first_last_name": "Vorname, Nachname",
|
||||
"invite_as": "Einladen als",
|
||||
"form_updated_successfully": "Formular erfolgreich aktualisiert.",
|
||||
"disable_attendees_confirmation_emails": "Standardmäßige Bestätigungs-E-Mails für Teilnehmer deaktivieren",
|
||||
@@ -1939,5 +1947,6 @@
|
||||
"insights_user_filter": "Benutzer: {{userName}}",
|
||||
"insights_subtitle": "Sehen Sie sich Buchungs-Insights über Ihre Termine an",
|
||||
"custom_plan": "Benutzerdefinierter Plan",
|
||||
"org_max_team_warnings": "Weitere Teams können Sie zu einem späteren Zeitpunkt hinzufügen.",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Zeichenketten über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -268,5 +268,6 @@
|
||||
"edit": "Επεξεργασία",
|
||||
"disable_guests": "Απενεργοποίηση επισκεπτών",
|
||||
"location_variable": "Τοποθεσία",
|
||||
"additional_notes_variable": "Πρόσθετες σημειώσεις",
|
||||
"insights_all_org_filter": "Όλα"
|
||||
}
|
||||
|
||||
@@ -244,6 +244,8 @@
|
||||
"all": "All",
|
||||
"yours": "Your account",
|
||||
"available_apps": "Available Apps",
|
||||
"available_apps_lower_case": "Available apps",
|
||||
"available_apps_desc": "You have no apps installed. View popular apps below and explore more in our <1>App Store</1>",
|
||||
"check_email_reset_password": "Check your email. We sent you a link to reset your password.",
|
||||
"finish": "Finish",
|
||||
"organization_general_description": "Manage settings for your team language and timezone",
|
||||
@@ -1181,6 +1183,7 @@
|
||||
"event_time_variable": "Event time",
|
||||
"timezone_variable": "Timezone",
|
||||
"location_variable": "Location",
|
||||
"additional_notes_variable": "Additional notes",
|
||||
"organizer_name_variable": "Organizer name",
|
||||
"app_upgrade_description": "In order to use this feature, you need to upgrade to a Pro account.",
|
||||
"invalid_number": "Invalid phone number",
|
||||
@@ -1868,6 +1871,9 @@
|
||||
"open_dialog_with_element_click": "Open your Cal dialog when someone clicks an element.",
|
||||
"need_help_embedding": "Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options.",
|
||||
"book_my_cal": "Book my Cal",
|
||||
"first_name": "First name",
|
||||
"last_name": "Last name",
|
||||
"first_last_name": "First name, Last name",
|
||||
"invite_as": "Invite as",
|
||||
"form_updated_successfully": "Form updated successfully.",
|
||||
"disable_attendees_confirmation_emails": "Disable default confirmation emails for attendees",
|
||||
@@ -1944,6 +1950,11 @@
|
||||
"insights_subtitle": "View booking insights across your events",
|
||||
"location_options": "{{locationCount}} location options",
|
||||
"custom_plan": "Custom Plan",
|
||||
"email_embed": "Email Embed",
|
||||
"add_times_to_your_email": "Add times to your Email",
|
||||
"select_time": "Select Time",
|
||||
"select_date": "Select Date",
|
||||
"see_all_available_times": "See all available times",
|
||||
"org_team_names_example": "e.g. Marketing Team",
|
||||
"org_team_names_example_1": "e.g. Marketing Team",
|
||||
"org_team_names_example_2": "e.g. Sales Team",
|
||||
|
||||
@@ -1178,6 +1178,7 @@
|
||||
"event_time_variable": "Hora del evento",
|
||||
"timezone_variable": "Zona Horaria",
|
||||
"location_variable": "Lugar",
|
||||
"additional_notes_variable": "Notas Adicionales",
|
||||
"organizer_name_variable": "Nombre del organizador",
|
||||
"app_upgrade_description": "Para poder usar esta función, necesita actualizarse a una cuenta Pro.",
|
||||
"invalid_number": "Número de teléfono no válido",
|
||||
|
||||
@@ -102,8 +102,8 @@
|
||||
"need_to_make_a_change": "Besoin de faire un changement ?",
|
||||
"new_event_scheduled": "Un nouvel événement a été planifié",
|
||||
"new_event_scheduled_recurring": "Un nouvel événement récurrent a été planifié",
|
||||
"invitee_email": "E-mail de l'invité(e)",
|
||||
"invitee_timezone": "Fuseau horaire de l'invité(e)",
|
||||
"invitee_email": "E-mail de l'invité",
|
||||
"invitee_timezone": "Fuseau horaire de l'invité",
|
||||
"time_left": "Temps restant",
|
||||
"event_type": "Type d'événement",
|
||||
"enter_meeting": "Rejoindre le rendez-vous",
|
||||
@@ -118,11 +118,11 @@
|
||||
"manage_this_team": "Gérer cette équipe",
|
||||
"team_info": "Informations sur l'équipe",
|
||||
"request_another_invitation_email": "Si vous préférez ne pas utiliser {{toEmail}} comme votre adresse e-mail {{appName}} ou si vous avez déjà un compte {{appName}}, veuillez demander une autre invitation à cette adresse.",
|
||||
"you_have_been_invited": "Vous avez été invité(e) à rejoindre l'équipe {{teamName}}",
|
||||
"you_have_been_invited": "Vous avez été invité à rejoindre l'équipe {{teamName}}",
|
||||
"user_invited_you": "{{user}} vous a invité à rejoindre l'{{entity}} {{team}} sur {{appName}}",
|
||||
"hidden_team_member_title": "Vous êtes masqué(e) dans cette équipe",
|
||||
"hidden_team_member_title": "Vous êtes masqué dans cette équipe",
|
||||
"hidden_team_member_message": "Votre place n'est pas payée, passez au plan Pro ou faites savoir au propriétaire de l'équipe qu'il peut payer votre place.",
|
||||
"hidden_team_owner_message": "Vous avez besoin d'un compte Pro pour utiliser les équipes, vous êtes masqué(e) jusqu'à ce que vous mettiez à niveau.",
|
||||
"hidden_team_owner_message": "Vous avez besoin d'un compte Pro pour utiliser les équipes, vous êtes masqué jusqu'à ce que vous mettiez à niveau.",
|
||||
"link_expires": "p.s. Il expire dans {{expiresIn}} heures.",
|
||||
"upgrade_to_per_seat": "Mise à niveau vers la place",
|
||||
"seat_options_doesnt_support_confirmation": "L'option de places ne prend pas en charge l'exigence de confirmation",
|
||||
@@ -165,7 +165,7 @@
|
||||
"view_public_page": "Voir la page publique",
|
||||
"copy_public_page_link": "Copier le lien de la page publique",
|
||||
"sign_out": "Se déconnecter",
|
||||
"add_another": "Ajouter un(e) autre",
|
||||
"add_another": "Ajouter un autre",
|
||||
"install_another": "Installer une autre",
|
||||
"until": "jusqu'au",
|
||||
"powered_by": "propulsé par",
|
||||
@@ -186,7 +186,7 @@
|
||||
"rejected": "Refusé",
|
||||
"unconfirmed": "Non confirmées",
|
||||
"guests": "Invités",
|
||||
"guest": "Invité(e)",
|
||||
"guest": "Invité",
|
||||
"web_conferencing_details_to_follow": "Les détails du rendez-vous sont à retrouver dans l'e-mail de confirmation.",
|
||||
"404_the_user": "Le nom d'utilisateur",
|
||||
"username": "Nom d'utilisateur",
|
||||
@@ -220,9 +220,9 @@
|
||||
"confirm_password": "Confirmer le mot de passe",
|
||||
"create_your_account": "Créez votre compte",
|
||||
"sign_up": "S'inscrire",
|
||||
"youve_been_logged_out": "Vous avez été déconnecté(e)",
|
||||
"youve_been_logged_out": "Vous avez été déconnecté",
|
||||
"hope_to_see_you_soon": "Nous espérons vous revoir bientôt !",
|
||||
"logged_out": "Déconnecté(e)",
|
||||
"logged_out": "Déconnecté",
|
||||
"please_try_again_and_contact_us": "Veuillez réessayer et nous contacter si le problème persiste.",
|
||||
"incorrect_2fa_code": "Le code de double authentification est incorrect.",
|
||||
"no_account_exists": "Aucun compte ne correspond à cette adresse email.",
|
||||
@@ -244,6 +244,8 @@
|
||||
"all": "Tous",
|
||||
"yours": "Votre compte",
|
||||
"available_apps": "Applications disponibles",
|
||||
"available_apps_lower_case": "Applications disponibles",
|
||||
"available_apps_desc": "Aucune application installée. Consultez les applications populaires ci-dessous et découvrez-en plus dans notre <1>App Store</1>.",
|
||||
"check_email_reset_password": "Vérifiez vos e-mails. Nous vous avons envoyé un lien pour réinitialiser votre mot de passe.",
|
||||
"finish": "Terminer",
|
||||
"organization_general_description": "Gérez les paramètres de langue et de fuseau horaire de votre équipe.",
|
||||
@@ -363,7 +365,7 @@
|
||||
"unconfirmed_bookings_tooltip": "Réservations non confirmées",
|
||||
"on": "le",
|
||||
"and": "et",
|
||||
"calendar_shows_busy_between": "Votre calendrier indique que vous êtes occupé(e) entre",
|
||||
"calendar_shows_busy_between": "Votre calendrier indique que vous êtes occupé entre",
|
||||
"troubleshoot": "Dépannage",
|
||||
"troubleshoot_description": "Comprendre pourquoi certains créneaux sont disponibles et d'autres sont bloquées.",
|
||||
"overview_of_day": "Voici un aperçu de votre journée le",
|
||||
@@ -388,7 +390,7 @@
|
||||
"and_end_at": "et se termine à",
|
||||
"light": "Clair",
|
||||
"dark": "Sombre",
|
||||
"automatically_adjust_theme": "Ajuster automatiquement l'apparence en fonction des préférences de l'invité(e)",
|
||||
"automatically_adjust_theme": "Ajuster automatiquement l'apparence en fonction des préférences de l'invité",
|
||||
"user_dynamic_booking_disabled": "Certains utilisateurs du groupe ont actuellement désactivé les réservations de groupe dynamiques",
|
||||
"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",
|
||||
@@ -767,7 +769,7 @@
|
||||
"business_days": "jours ouvrés",
|
||||
"set_address_place": "Définir une adresse ou un lieu",
|
||||
"set_link_meeting": "Définir un lien vers le rendez-vous",
|
||||
"cal_invitee_phone_number_scheduling": "{{appName}} demandera à votre invité(e) d'entrer un numéro de téléphone avant de faire une réservation.",
|
||||
"cal_invitee_phone_number_scheduling": "{{appName}} demandera à votre invité d'entrer un numéro de téléphone avant de faire une réservation.",
|
||||
"cal_provide_google_meet_location": "{{appName}} fournira un lien Google Meet.",
|
||||
"cal_provide_zoom_meeting_url": "{{appName}} fournira un lien de rendez-vous Zoom.",
|
||||
"cal_provide_tandem_meeting_url": "{{appName}} fournira un lien de rendez-vous Tandem.",
|
||||
@@ -944,7 +946,7 @@
|
||||
"12_hour_short": "12 h",
|
||||
"24_hour_short": "24 h",
|
||||
"redirect_success_booking": "Rediriger après la réservation",
|
||||
"you_are_being_redirected": "Vous allez être redirigé(e) vers {{ url }} dans $t(second, {\"count\": {{seconds}} }).",
|
||||
"you_are_being_redirected": "Vous allez être redirigé vers {{ url }} dans $t(second, {\"count\": {{seconds}} }).",
|
||||
"external_redirect_url": "https://exemple.com/redirection-page-de-validation",
|
||||
"redirect_url_description": "Redirigez l'utilisateur vers un lien personnalisé après une réservation réussie.",
|
||||
"duplicate": "Dupliquer",
|
||||
@@ -992,7 +994,7 @@
|
||||
"reason_for_reschedule": "Raison de la replanification",
|
||||
"book_a_new_time": "Réserver un nouveau créneau",
|
||||
"reschedule_request_sent": "Demande de replanification envoyée",
|
||||
"reschedule_modal_description": "Ceci annulera le rendez-vous planifié et demandera à l'invité(e) de choisir un nouveau créneau.",
|
||||
"reschedule_modal_description": "Ceci annulera le rendez-vous planifié et demandera à l'invité de choisir un nouveau créneau.",
|
||||
"reason_for_reschedule_request": "Raison de la demande de replanification",
|
||||
"send_reschedule_request": "Demander une replanification ",
|
||||
"edit_booking": "Modifier la réservation",
|
||||
@@ -1036,7 +1038,7 @@
|
||||
"generate_api_key_description": "Générer une clé API à utiliser avec {{appName}} sur",
|
||||
"your_unique_api_key": "Votre clé API unique",
|
||||
"copy_safe_api_key": "Copiez cette clé API et conservez-la dans un endroit sûr. Si vous perdez cette clé, vous devrez en générer une nouvelle.",
|
||||
"zapier_setup_instructions": "<0>Connectez-vous à votre compte Zapier et créez un nouveau Zap.</0><1>Sélectionnez Cal.com comme application Trigger. Choisissez également un événement Trigger.</1><2>Choisissez votre compte, puis saisissez votre clé API unique.</2><3>Testez votre Trigger.</3><4>Vous êtes prêt(e) !</4>",
|
||||
"zapier_setup_instructions": "<0>Connectez-vous à votre compte Zapier et créez un nouveau Zap.</0><1>Sélectionnez Cal.com comme application Trigger. Choisissez également un événement Trigger.</1><2>Choisissez votre compte, puis saisissez votre clé API unique.</2><3>Testez votre Trigger.</3><4>Vous êtes prêt !</4>",
|
||||
"install_zapier_app": "Veuillez d'abord installer l'application Zapier dans l'App Store.",
|
||||
"connect_apple_server": "Se connecter au serveur d'Apple",
|
||||
"calendar_url": "Lien du calendrier",
|
||||
@@ -1180,6 +1182,7 @@
|
||||
"event_time_variable": "Heure de l'événement",
|
||||
"timezone_variable": "Fuseau horaire",
|
||||
"location_variable": "Lieu",
|
||||
"additional_notes_variable": "Notes supplémentaires",
|
||||
"organizer_name_variable": "Nom de l'organisateur",
|
||||
"app_upgrade_description": "Pour pouvoir utiliser cette fonctionnalité, vous devez passer à un compte Pro.",
|
||||
"invalid_number": "Numéro de téléphone invalide",
|
||||
@@ -1360,7 +1363,7 @@
|
||||
"appearance_team_description": "Gérez les paramètres d'apparence de vos réservations d'équipe.",
|
||||
"only_owner_change": "Seul le propriétaire de cette équipe peut apporter des modifications à la réservation de l'équipe ",
|
||||
"team_disable_cal_branding_description": "Supprimez toutes les références à la marque {{appName}}, par exemple « Propulsé par {{appName}} ».",
|
||||
"invited_by_team": "{{teamName}} vous a invité(e) à rejoindre son équipe en tant que {{role}}",
|
||||
"invited_by_team": "{{teamName}} vous a invité à rejoindre son équipe en tant que {{role}}",
|
||||
"token_invalid_expired": "Le token est invalide ou a expiré.",
|
||||
"exchange_add": "Se connecter à Microsoft Exchange",
|
||||
"exchange_authentication": "Méthode d'authentification",
|
||||
@@ -1574,7 +1577,7 @@
|
||||
"date_overrides_subtitle": "Ajoutez les dates où vos disponibilités changent de vos heures quotidiennes.",
|
||||
"date_overrides_info": "Les superpositions de dates sont archivées automatiquement une fois la date passée",
|
||||
"date_overrides_dialog_which_hours": "À quelles heures êtes-vous disponible ?",
|
||||
"date_overrides_dialog_which_hours_unavailable": "À quelles heures êtes-vous occupé(e) ?",
|
||||
"date_overrides_dialog_which_hours_unavailable": "À quelles heures êtes-vous occupé ?",
|
||||
"date_overrides_dialog_title": "Sélectionnez les dates à superposer",
|
||||
"date_overrides_unavailable": "Indisponible toute la journée",
|
||||
"date_overrides_mark_all_day_unavailable_one": "Marquer comme indisponible (toute la journée)",
|
||||
@@ -1669,7 +1672,7 @@
|
||||
"email_no_user_step_three": "Définissez vos disponibilités",
|
||||
"email_no_user_step_four": "Rejoignez {{teamName}}",
|
||||
"email_no_user_signoff": "Bonne planification de la part de l'équipe {{appName}}",
|
||||
"impersonation_user_tip": "Vous êtes sur le point d'emprunter l'identité d'un utilisateur, ce qui signifie que vous pouvez effectuer des modifications en son nom. S'il vous plaît, soyez prudent(e).",
|
||||
"impersonation_user_tip": "Vous êtes sur le point d'emprunter l'identité d'un utilisateur, ce qui signifie que vous pouvez effectuer des modifications en son nom. S'il vous plaît, soyez prudent.",
|
||||
"available_variables": "Variables disponibles",
|
||||
"scheduler": "{Scheduler}",
|
||||
"no_workflows": "Aucun workflow",
|
||||
@@ -1848,7 +1851,7 @@
|
||||
"thirty_days": "30 jours",
|
||||
"three_months": "3 mois",
|
||||
"one_year": "1 an",
|
||||
"team_invite_received": "Vous avez été invité(e) à rejoindre {{teamName}}",
|
||||
"team_invite_received": "Vous avez été invité à rejoindre {{teamName}}",
|
||||
"currency_string": "{{amount, currency}}",
|
||||
"charge_card_dialog_body": "Vous êtes sur le point de facturer {{amount, currency}} au participant. Voulez-vous vraiment continuer ?",
|
||||
"charge_attendee": "Facturer {{amount, currency}} au participant",
|
||||
@@ -1866,6 +1869,9 @@
|
||||
"open_dialog_with_element_click": "Ouvre votre boîte de dialogue Cal lorsque quelqu'un clique sur un élément.",
|
||||
"need_help_embedding": "Besoin d'aide ? Consultez nos guides pour intégrer Cal sur Wix, Squarespace ou WordPress, consultez nos questions fréquentes ou explorez les options d'intégration avancées.",
|
||||
"book_my_cal": "Prendre rendez-vous",
|
||||
"first_name": "Prénom",
|
||||
"last_name": "Nom",
|
||||
"first_last_name": "Prénom, Nom",
|
||||
"invite_as": "Inviter en tant que",
|
||||
"form_updated_successfully": "Formulaire mis à jour avec succès.",
|
||||
"disable_attendees_confirmation_emails": "Désactiver les e-mails de confirmation par défaut pour les participants",
|
||||
@@ -1942,6 +1948,7 @@
|
||||
"insights_subtitle": "Visualisez les statistiques de réservation à travers vos événements",
|
||||
"location_options": "{{locationCount}} options de lieu",
|
||||
"custom_plan": "Plan personnalisé",
|
||||
"email_embed": "E-mail intégré",
|
||||
"org_team_names_example": "p. ex. Équipe marketing",
|
||||
"org_team_names_example_1": "p. ex. Équipe marketing",
|
||||
"org_team_names_example_2": "p. ex. Équipe de vente",
|
||||
|
||||
@@ -14,15 +14,20 @@
|
||||
"verify_email_subject": "{{appName}}: אמת את החשבון",
|
||||
"check_your_email": "בדוק את הדוא״ל שלך",
|
||||
"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": "הנה הקישור, למקרה שאינך אוהב/ת ללחוץ על כפתורים:",
|
||||
"email_sent": "מייל נשלח בהצלחה",
|
||||
"event_declined_subject": "נדחה: {{title}} ב- {{date}}",
|
||||
"event_cancelled_subject": "בוטל: {{title}} ב-{{date}}",
|
||||
"event_request_declined": "בקשת האירוע שלך נדחתה",
|
||||
"event_request_declined_recurring": "בקשת האירוע החוזר שלך נדחתה",
|
||||
"event_request_cancelled": "האירוע המתוכנן שלך בוטל",
|
||||
"organizer": "מארגן",
|
||||
"need_to_reschedule_or_cancel": "רוצה לקבוע מועד חדש או לבטל?",
|
||||
"no_options_available": "אין אפשרויות זמינות",
|
||||
"cancellation_reason": "הסיבה לביטול (לא חובה)",
|
||||
"cancellation_reason_placeholder": "מדוע בחרת לבטל? (לא חובה)",
|
||||
"rejection_reason": "הסיבה לדחייה",
|
||||
@@ -82,7 +87,9 @@
|
||||
"your_meeting_has_been_booked": "הפגישה שלך הוזמנה",
|
||||
"event_type_has_been_rescheduled_on_time_date": "{{title}} תוזמן מחדש לתאריך {{date}}.",
|
||||
"event_has_been_rescheduled": "עודכן – מועד האירוע שלך השתנה",
|
||||
"request_reschedule_subtitle": "{{organizer}} ביטל/ה את ההזמנה וביקש/ה שתבחר/י מועד אחר.",
|
||||
"request_reschedule_title_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}}",
|
||||
@@ -111,6 +118,7 @@
|
||||
"team_info": "מידע על הצוות",
|
||||
"request_another_invitation_email": "אם אתה מעדיף לא להשתמש בכתובת {{toEmail}} ככתובת הדוא\"ל שלך עבור {{appName}} או שכבר יש לך חשבון {{appName}}, יש לבקש לקבל הזמנה נוספת לכתובת הדוא\"ל הרצויה.",
|
||||
"you_have_been_invited": "הוזמנת להצטרף לצוות {{teamName}}",
|
||||
"user_invited_you": "{{user}} הזמין/ה אותך להצטרף ל{{entity}} {{team}} ב-{{appName}}",
|
||||
"hidden_team_member_title": "אתה מוסתר בצוות זה",
|
||||
"hidden_team_member_message": "לא בוצע תשלום עבור המקום שלך. ניתן לשדרג ל-PRO או ליידע את הבעלים של הצוות שהוא או היא יכולים לשלם עבור המקום שלך.",
|
||||
"hidden_team_owner_message": "נדרש חשבון Pro כדי להשתמש בתכונות הצוותים. תהיה מוסתר עד שתבצע שידרוג.",
|
||||
@@ -121,6 +129,8 @@
|
||||
"team_upgrade_banner_description": "אנחנו מודים לך על כך שניסית את החבילה החדשה שלנו לצוותים. שמנו לב שהצוות שלך, \"{{teamName}}\", זקוק לשדרוג.",
|
||||
"upgrade_banner_action": "כאן משדרגים",
|
||||
"team_upgraded_successfully": "הצוות שלך שודרג בהצלחה!",
|
||||
"org_upgrade_banner_description": "אנחנו מודים לך על כך שניסית את החבילה החדשה שלנו לארגונים. שמנו לב שהארגון שלך, \"{{teamName}}\", זקוק לשדרוג.",
|
||||
"org_upgraded_successfully": "השדרוג של הארגון שלך בוצע בהצלחה!",
|
||||
"use_link_to_reset_password": "נא להשתמש בקישור הבא כדי לאפס את הסיסמה",
|
||||
"hey_there": "שלום,",
|
||||
"forgot_your_password_calcom": "שכחתה את הסיסמה? - {{appName}}",
|
||||
@@ -187,6 +197,7 @@
|
||||
"blog": "בלוג",
|
||||
"blog_description": "לעיון בחדשות ובמאמרים האחרונים שלנו",
|
||||
"join_our_community": "הצטרף לקהילה שלנו",
|
||||
"join_our_discord": "מזמינים אותך להצטרף ל-Discord שלנו",
|
||||
"404_claim_entity_user": "קבל את שם המשתמש שלך והתחל לקבוע מועדים לאירועים",
|
||||
"popular_pages": "דפים פופולריים",
|
||||
"register_now": "להירשם עכשיו",
|
||||
@@ -230,11 +241,14 @@
|
||||
"done": "בוצע",
|
||||
"all_done": "זהו, סיימת!",
|
||||
"all": "כל האפליקציות",
|
||||
"yours": "החשבון שלך",
|
||||
"available_apps": "אפליקציות זמינות",
|
||||
"check_email_reset_password": "בדוק/י בתיבת הדוא\"ל – שלחנו לך קישור לאיפוס הסיסמה.",
|
||||
"finish": "סיום",
|
||||
"organization_general_description": "ניהול הגדרות השפה ואזור הזמן של הצוות",
|
||||
"few_sentences_about_yourself": "מספר משפטים אודותיך. המידע הזה יופיע בדף ה-URL האישי שלך.",
|
||||
"nearly_there": "כמעט סיימנו!",
|
||||
"nearly_there_instructions": "דבר אחרון: תיאור קצר אודותיך בתוספת תמונה ממש עוזרים להשיג הזמנות ומאפשרים לאנשים לדעת עם מי הם עומדים להיפגש.",
|
||||
"set_availability_instructions": "הגדר טווחי זמן שבהם אתה זמין באופן קבוע. ניתן יהיה ליצור טווחי זמן נוספים מאוחר יותר ולהקצות אותם ללוחות שנה אחרים.",
|
||||
"set_availability": "ציין את הזמינות שלך",
|
||||
"continue_without_calendar": "להמשיך בלי לוח שנה",
|
||||
@@ -294,6 +308,14 @@
|
||||
"bookerlayout_default_title": "תצוגת ברירת מחדל",
|
||||
"bookerlayout_description": "אתה יכול לבחור כמה והמתזמנים שלך יכולים לשנות תצוגות.",
|
||||
"bookerlayout_user_settings_title": "פריסת תזמון",
|
||||
"bookerlayout_user_settings_description": "אפשר לבחור מזמינים מרובים והמזמינים יכולים לשנות תצוגות. ניתן לעקוף זאת על בסיס כל אירוע לחוד.",
|
||||
"bookerlayout_month_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>.",
|
||||
"unexpected_error_try_again": "אירעה שגיאה בלתי צפויה. נא לנסות שוב.",
|
||||
"sunday_time_error": "שעה לא תקפה ביום ראשון",
|
||||
"monday_time_error": "שעה לא תקפה ביום שני",
|
||||
@@ -306,6 +328,7 @@
|
||||
"error_end_time_next_day": "שעת הסיום לא יכולה לחרוג מ-24 שעות",
|
||||
"back_to_bookings": "בחזרה להזמנות",
|
||||
"free_to_pick_another_event_type": "ניתן לבחור אירוע אחר בכל עת.",
|
||||
"cancelled": "בוטל",
|
||||
"cancellation_successful": "פעולת הביטול בוצעה בהצלחה",
|
||||
"really_cancel_booking": "בטוח שברצונך לבטל את ההזמנה?",
|
||||
"cannot_cancel_booking": "לא ניתן לבטל את ההזמנה הזו",
|
||||
@@ -314,6 +337,12 @@
|
||||
"cancelling_event_recurring": "האירוע הוא מופע אחד מתוך אירוע חוזר.",
|
||||
"cancelling_all_recurring": "כל אלה הם יתר המופעים של האירוע החוזר.",
|
||||
"error_with_status_code_occured": "אירעה שגיאה עם קוד הסטטוס {{status}}.",
|
||||
"error_event_type_url_duplicate": "כבר קיים סוג אירוע עם כתובת ה-URL הזו.",
|
||||
"error_event_type_unauthorized_create": "אין לך אפשרות ליצור את האירוע הזה",
|
||||
"error_event_type_unauthorized_update": "אין לך אפשרות לערוך את האירוע הזה",
|
||||
"error_workflow_unauthorized_create": "אין לך אפשרות ליצור את תהליך העבודה הזה",
|
||||
"error_schedule_unauthorized_create": "אין לך אפשרות ליצור את לוח הזמנים הזה",
|
||||
"booking_already_cancelled": "ההזמנה הזו כבר בוטלה",
|
||||
"booking_already_accepted_rejected": "ההזמנה הזו כבר אושרה או נדחתה",
|
||||
"go_back_home": "חזרה אל מסך הבית",
|
||||
"or_go_back_home": "או חזור אל מסך הבית",
|
||||
@@ -323,10 +352,12 @@
|
||||
"no_status_bookings_yet_description": "אין לך הזמנות בסטטוס {{status}}. {{description}}",
|
||||
"event_between_users": "{{eventName}} בין {{host}} ו-{{attendeeName}}",
|
||||
"bookings": "הזמנות",
|
||||
"booking_not_found": "ההזמנה לא נמצאה",
|
||||
"bookings_description": "צפה באירועים הקרובים ובאירועים שעברו שהוזמנו דרך קישורי סוגי האירועים שלך.",
|
||||
"upcoming_bookings": "ברגע שמישהו יקבע איתך פגישה, המועד שנקבע יוצג כאן.",
|
||||
"recurring_bookings": "ברגע שמישהו יקבע איתך פגישה חוזרת, המועד שנקבע יוצג כאן.",
|
||||
"past_bookings": "ההזמנות הקודמות שלך יוצגו כאן.",
|
||||
"cancelled_bookings": "ההזמנות המבוטלות שלך יוצגו כאן.",
|
||||
"unconfirmed_bookings": "ההזמנות שלך שלא אושרו יוצגו כאן.",
|
||||
"unconfirmed_bookings_tooltip": "הזמנות שלא אושרו",
|
||||
"on": "בתאריך",
|
||||
@@ -360,15 +391,19 @@
|
||||
"user_dynamic_booking_disabled": "אצל חלק מהמשתמשים בקבוצה מושבתת כרגע האפשרות של הזמנות קבוצתיות דינמיות",
|
||||
"allow_dynamic_booking_tooltip": "קישורי הזמנות קבוצתיות שניתן ליצור באופן דינמי על ידי הוספת שמות משתמשים מרובים עם '+', למשל: '{{appName}}/bailey+peer'",
|
||||
"allow_dynamic_booking": "לאפשר למשתתפים לשריין איתך פגישה באמצעות הזמנות קבוצתיות דינמיות",
|
||||
"dynamic_booking": "קישורים לקבוצות דינמיות",
|
||||
"email": "דוא\"ל",
|
||||
"email_placeholder": "iisraeli@example.com",
|
||||
"full_name": "שם מלא",
|
||||
"browse_api_documentation": "עיון במסמכי ממשק תכנות היישומים (API) שלנו",
|
||||
"leverage_our_api": "מומלץ להיעזר בממשק תכנות היישומים (API) שלנו לקבלת שליטה מלאה ויכולת התאמה אישית.",
|
||||
"create_webhook": "יצירת Webhook",
|
||||
"booking_cancelled": "ההזמנה בוטלה",
|
||||
"booking_rescheduled": "מועד ההזמנה השתנה",
|
||||
"recording_ready": "הקישור להורדת ההקלטה מוכן",
|
||||
"booking_created": "ההזמנה נוצרה",
|
||||
"booking_rejected": "ההזמנה נדחתה",
|
||||
"booking_requested": "ההזמנה התבקשה",
|
||||
"meeting_ended": "הפגישה הסתיימה",
|
||||
"form_submitted": "הטופס נשלח",
|
||||
"event_triggers": "גורמים מפעילים של אירועים",
|
||||
@@ -378,6 +413,8 @@
|
||||
"team_webhooks": "רכיבי Webhook של הצוות",
|
||||
"create_new_webhook_to_account": "יצירת Webhook חדש לחשבון",
|
||||
"new_webhook": "Webhook חדש",
|
||||
"receive_cal_meeting_data": "קבל/י נתוני פגישות מ-{{appName}} בכתובת URL שציינת ובזמן אמת, כאשר נקבע מועד לאירוע או כאשר אירוע מבוטל.",
|
||||
"receive_cal_event_meeting_data": "קבל/י נתוני פגישות מ-{{appName}} בכתובת URL שציינת ובזמן אמת, כאשר נקבע מועד לאירוע זה או כאשר הוא מבוטל.",
|
||||
"responsive_fullscreen_iframe": "iframe רספונסיבי על מסך מלא",
|
||||
"loading": "טוען...",
|
||||
"deleting": "מתבצעת מחיקה...",
|
||||
@@ -440,6 +477,7 @@
|
||||
"max_limit_allowed_hint": "חייב להיות באורך של עד {{limit}} תווים",
|
||||
"invalid_password_hint": "הסיסמה חייבת להיות באורך של {{passwordLength}} תווים לפחות ולכלול לפחות ספרה אחת ושילוב של אותיות רישיות וקטנות",
|
||||
"incorrect_password": "הסיסמה שגויה.",
|
||||
"incorrect_email_password": "כתובת הדוא\"ל או הסיסמה שגויות.",
|
||||
"use_setting": "השתמש בהגדרה",
|
||||
"am_pm": "לפנה\"צ/אחה\"צ",
|
||||
"time_options": "אפשרויות זמן",
|
||||
@@ -467,6 +505,7 @@
|
||||
"additional_guests": "הוספת אורחים",
|
||||
"your_name": "שמך",
|
||||
"your_full_name": "שמך המלא",
|
||||
"no_name": "ללא שם",
|
||||
"email_address": "כתובת דוא\"ל",
|
||||
"enter_valid_email": "יש להזין כתובת דוא״ל תקינה",
|
||||
"location": "מיקום",
|
||||
@@ -510,10 +549,13 @@
|
||||
"your_team_updated_successfully": "הצוות שלך עודכן בהצלחה.",
|
||||
"about": "אודות",
|
||||
"team_description": "מספר משפטים אודות הצוות. המידע הזה יופיע בדף ה-URL של הצוות.",
|
||||
"org_description": "מספר משפטים אודות הארגון. הם יופיעו בדף עם כתובת ה-URL של הארגון.",
|
||||
"members": "חברים",
|
||||
"organization_members": "חברי הארגון",
|
||||
"member": "חבר/ת",
|
||||
"number_member_one": "חבר {{count}}",
|
||||
"number_member_other": "{{count}} חברים",
|
||||
"number_selected": "{{count}} נבחרו",
|
||||
"owner": "בעלים",
|
||||
"admin": "מנהל/ת מערכת",
|
||||
"administrator_user": "משתמש המוגדר כמנהל מערכת",
|
||||
@@ -544,6 +586,8 @@
|
||||
"confirm_all": "לאשר הכל",
|
||||
"disband_team": "פיזור הצוות",
|
||||
"disband_team_confirmation_message": "בטוח שברצונך לפזר את הצוות הזה? כל מי ששיתפת איתו את הקישור לצוות לא יוכל להזמין יותר באמצעותו.",
|
||||
"disband_org": "פיזור הארגון",
|
||||
"disband_org_confirmation_message": "בטוח שברצונך לפזר את הארגון הזה? כל הצוותים והחברים יימחקו.",
|
||||
"remove_member_confirmation_message": "בטוח שברצונך להסיר חבר/ה זה/ו מהצוות?",
|
||||
"confirm_disband_team": "כן, לפזר את הצוות",
|
||||
"confirm_remove_member": "כן, להסיר את החבר/ה",
|
||||
@@ -618,6 +662,7 @@
|
||||
"event_type_updated_successfully": "עדכון האירוע מסוג {{eventTypeTitle}} בוצע בהצלחה",
|
||||
"event_type_deleted_successfully": "מחיקת סוג האירוע בוצעה בהצלחה",
|
||||
"hours": "שעות",
|
||||
"people": "אנשים",
|
||||
"your_email": "הדוא\"ל שלך",
|
||||
"change_avatar": "שינוי אווטאר",
|
||||
"language": "שפה",
|
||||
@@ -650,9 +695,11 @@
|
||||
"create_new_team": "יצירת צוות חדש",
|
||||
"open_invitations": "הזמנות פתוחות",
|
||||
"new_team": "צוות חדש",
|
||||
"create_first_team_and_invite_others": "צור/צרי את הצוות הראשון שלך והזמן/י משתמשים אחרים לעבוד יחד.",
|
||||
"create_team_to_get_started": "צור צוות כדי להתחיל",
|
||||
"teams": "צוותים",
|
||||
"team": "צוות",
|
||||
"organization": "ארגון",
|
||||
"team_billing": "חיוב צוותים",
|
||||
"team_billing_description": "ניהול החיוב עבור הצוות",
|
||||
"upgrade_to_flexible_pro_title": "שינינו את החיוב בעבור צוותים",
|
||||
@@ -727,6 +774,7 @@
|
||||
"cal_provide_huddle01_meeting_url": "{{appName}} יספק כתובת קישור לפגישת וידאו web3 ב- Huddle01.",
|
||||
"cal_provide_teams_meeting_url": "{{appName}} יספק כתובת קישור לפגישת MS Teams. הערה: חייב להיות חשבון של מקום עבודה או בית ספר",
|
||||
"require_payment": "דרישת תשלום",
|
||||
"you_need_to_add_a_name": "יש להוסיף שם",
|
||||
"commission_per_transaction": "עמלה לעסקה",
|
||||
"event_type_updated_successfully_description": "סוג האירוע שלך עודכן בהצלחה.",
|
||||
"hide_event_type": "הסתר את סוג האירוע",
|
||||
@@ -817,18 +865,25 @@
|
||||
"installed_apps": "אפליקציות מותקנות",
|
||||
"free_to_use_apps": "חינם",
|
||||
"no_category_apps": "אין אפליקציות בקטגוריה {{category}}",
|
||||
"all_apps": "כל האפליקציות",
|
||||
"no_category_apps_description_calendar": "הוסף/י אפליקציית לוח שנה כדי לבדוק אם יש התנגשויות על מנת למנוע כפל הזמנות",
|
||||
"no_category_apps_description_conferencing": "נסה/י להוסיף אפליקציית שיחות ועידה לצורך שיחות וידאו עם הלקוחות",
|
||||
"no_category_apps_description_payment": "הוסף/י אפליקציית תשלומים על מנת להפוך את ביצוע העסקאות בינך לבין הלקוחות לפשוט יותר",
|
||||
"no_category_apps_description_analytics": "להוסיף אפליקציה לניתוח נתונים עבור דפי ההזמנות שלך",
|
||||
"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_crm": "הוסף/י אפליקציית CRM כדי לנהל מעקב אחר האנשים שנפגשת איתם",
|
||||
"installed_app_calendar_description": "הגדר/הגדירח את לוחות השנה כדי לבדוק אם יש התנגשויות על מנת למנוע כפל הזמנות.",
|
||||
"installed_app_payment_description": "הגדר/הגידירי את השירותים לעיבוד תשלומים שבהם יש להשתמש לחיוב הלקוחות.",
|
||||
"installed_app_analytics_description": "הגדרת האפליקציות לניתוח נתונים שבהן יש להשתמש עבור דפי ההזמנות שלך",
|
||||
"installed_app_other_description": "כל האפליקציות המותקנות שלך מקטגוריות אחרות.",
|
||||
"installed_app_conferencing_description": "הגדר/י את האפליקציות לשיחות ועידה שבהן ברצונך להשתמש",
|
||||
"installed_app_automation_description": "הגדרת אפליקציות האוטומציה שבהן ברצונך להשתמש",
|
||||
"installed_app_web3_description": "הגדר באיזה אפליקציות web3 להשתמש בעמודי תזמון הפגישות שלך",
|
||||
"installed_app_messaging_description": "הגדר/י את האפליקציות להעברת הודעות שבהן ברצונך להשתמש להגדרת עדכונים ותזכורות",
|
||||
"installed_app_crm_description": "הגדר/י את אפליקציות ה-CRM שבהן ברצונך להשתמש לניהול מעקב אחר האנשים שנפגשת איתם",
|
||||
"analytics": "ניתוח נתונים",
|
||||
"empty_installed_apps_headline": "אין אפליקציות מותקנות",
|
||||
"empty_installed_apps_description": "יישומים מאפשרים לך לשפר את תהליכי העבודה שלך ואת פשטות ניהול התזמונים שלך משמעותית.",
|
||||
@@ -854,6 +909,7 @@
|
||||
"create_events_on": "ליצור אירועים ב-",
|
||||
"enterprise_license": "תכונה זו זמינה רק במינוי Enterprise",
|
||||
"enterprise_license_description": "כדי להפעיל את יכולת זו, בקש ממנהל לגשת אל הקישור {{setupUrl}} והקלדת הרישיון. אם כבר יש רישיון מוגדר, צור קשר עם {{supportMail}} לעזרה.",
|
||||
"enterprise_license_development": "ניתן לבדוק את התכונה הזו בשלב פיתוח. לשימוש בשלב הייצור, מנהל/ת מערכת צריך/ה לעבור אל <2>/auth/setup</2> ולהזין מפתח רישיון.",
|
||||
"missing_license": "חסר רישיון",
|
||||
"signup_requires": "נדרש רישיון לשימוש מסחרי",
|
||||
"signup_requires_description": "בשלב זה, {{companyName}} אינה מציעה גרסת קוד פתוח חינמית של דף ההרשמה. לקבלת גישה מלאה לרכיבי ההרשמה, תצטרך לקנות רישיון לשימוש מסחרי. לצרכים אישיים אנו ממליצים על Prisma Data Platform או כל ממשק Postgres אחר ליצירת חשבונות.",
|
||||
@@ -923,6 +979,7 @@
|
||||
"confirm_delete_api_key": "ביטול מפתח ממשק תכנות יישומים (API) זה",
|
||||
"revoke_api_key": "ביטול מפתח ממשק תכנות יישומים (API)",
|
||||
"api_key_copied": "מפתח ממשק תכנות היישומים (API) הועתק!",
|
||||
"api_key_expires_on": "התוקף של מפתח ה-API יסתיים ב-",
|
||||
"delete_api_key_confirm_title": "להסיר את מפתח ממשק תכנות היישומים (API) הזה מהחשבון לצמיתות?",
|
||||
"copy": "העתק",
|
||||
"expire_date": "תאריך פקיעת תוקף",
|
||||
@@ -945,6 +1002,9 @@
|
||||
"user_impersonation_heading": "התחזות למשתמשים",
|
||||
"user_impersonation_description": "מצב זה מאפשר לצוות התמיכה שלנו להתחבר באופן זמני לחשבונך כדי שנוכל לפתור במהירות את הבעיות שתדווח/י לנו עליהם.",
|
||||
"team_impersonation_description": "מאפשר לבעלים של הצוות/מנהלים להיכנס זמנית בשמך.",
|
||||
"make_team_private": "להגדיר את הצוות כפרטי",
|
||||
"make_team_private_description": "כשההגדרה הזו מופעלת, חברי הצוות לא יוכלו לראות חברי צוות אחרים.",
|
||||
"you_cannot_see_team_members": "אין לך אפשרות לראות את כל חברי הצוות של צוות פרטי.",
|
||||
"allow_booker_to_select_duration": "אפשר למזמין לבחור את משך הזמן",
|
||||
"impersonate_user_tip": "כל השימושים בתכונה זו נבדקים.",
|
||||
"impersonating_user_warning": "בהתחזות לשם המשתמש \"{{user}}\".",
|
||||
@@ -1012,16 +1072,21 @@
|
||||
"event_location": "מיקום האירוע",
|
||||
"reschedule_optional": "הסיבה לשינוי המועד (לא חובה)",
|
||||
"reschedule_placeholder": "אפשר לאחרים לדעת מדוע עליך לשנות את המועד",
|
||||
"event_cancelled": "אירוע זה מבוטל",
|
||||
"emailed_information_about_cancelled_event": "שלחנו לך ולשאר המשתתפים הודעת דוא״ל כדי ליידע אותם על כך.",
|
||||
"this_input_will_shown_booking_this_event": "הקלט יוצג בעת הזמנת אירוע זה",
|
||||
"meeting_url_in_conformation_email": "כתובת ה-URL של הפגישה נמצאת בהודעת הדוא״ל שנשלחה לאישור הפגישה",
|
||||
"url_start_with_https": "כתובת ה-URL צריכה להתחיל עם הקידומת http:// או https://",
|
||||
"number_provided": "מספר הטלפון יסופק",
|
||||
"before_event_trigger": "לפני שהאירוע מתחיל",
|
||||
"event_cancelled_trigger": "כאשר אירוע מבוטל",
|
||||
"new_event_trigger": "כאשר אירוע חדש מוזמן",
|
||||
"email_host_action": "שלח מייל למארח",
|
||||
"email_attendee_action": "שליחת דוא\"ל למשתתפים",
|
||||
"sms_attendee_action": "שליחת SMS למשתתף",
|
||||
"sms_number_action": "שלח SMS למספר מסוים",
|
||||
"whatsapp_number_action": "שליחת Whatsapp למספר מסוים",
|
||||
"whatsapp_attendee_action": "שליחת Whatsapp למשתתף",
|
||||
"workflows": "תהליכים",
|
||||
"new_workflow_btn": "תהליך עבודה חדש",
|
||||
"add_new_workflow": "הוסף תהליך חדש",
|
||||
@@ -1085,6 +1150,8 @@
|
||||
"choose_template": "בחירת תבנית",
|
||||
"custom": "התאמה אישית",
|
||||
"reminder": "תזכורת",
|
||||
"rescheduled": "נקבע מועד חדש",
|
||||
"completed": "הושלם",
|
||||
"reminder_email": "תזכורת: {{eventType}} עם {{name}} בתאריך {{date}}",
|
||||
"not_triggering_existing_bookings": "לא יופעל עבור הזמנות קיימות מאחר שהמשתמש יתבקש למסור מספר טלפון בעת הזמנת האירוע.",
|
||||
"minute_one": "{{count}} דקה",
|
||||
@@ -1111,6 +1178,7 @@
|
||||
"event_time_variable": "מועד האירוע",
|
||||
"timezone_variable": "אזור זמן",
|
||||
"location_variable": "מיקום",
|
||||
"additional_notes_variable": "הערות נוספות",
|
||||
"organizer_name_variable": "שם המארגן/ת",
|
||||
"app_upgrade_description": "כדי להשתמש בתכונה זו, עליך לשדרג לחשבון Pro.",
|
||||
"invalid_number": "מספר טלפון לא תקין",
|
||||
@@ -1182,6 +1250,7 @@
|
||||
"start_of_week": "תחילת השבוע",
|
||||
"recordings_title": "הקלטות",
|
||||
"recording": "הקלטה",
|
||||
"happy_scheduling": "תזמון נעים",
|
||||
"select_calendars": "בחר את לוחות השנה שבהם ברצונך לבדוק אם יש התנגשויות, כדי למנוע כפל הזמנות.",
|
||||
"check_for_conflicts": "בדיקת התנגשויות",
|
||||
"view_recordings": "צפייה בהקלטות",
|
||||
@@ -1223,6 +1292,7 @@
|
||||
"conferencing_description": "הוסף/הוסיפי את האפליקציות האהובות עליך/ייך לשיחות ועידה לשימוש בפגישות",
|
||||
"add_conferencing_app": "הוספת אפליקציה לשיחות ועידה",
|
||||
"password_description": "נהל/י את ההגדרות של סיסמאות החשבון שלך",
|
||||
"set_up_two_factor_authentication": "הגדרת אימות דו-גורמי",
|
||||
"we_just_need_basic_info": "אנחנו זקוקים רק לפרטים בסיסיים כדי להגדיר את הפרופיל שלך.",
|
||||
"skip": "לדלג",
|
||||
"do_this_later": "לעשות זאת מאוחר יותר",
|
||||
@@ -1236,6 +1306,8 @@
|
||||
"connect_analytics_apps": "קישור אפליקציות ניתוח נתונים",
|
||||
"connect_other_apps": "חיבור אפליקציות אחרות",
|
||||
"connect_web3_apps": "חבר אפליקציות web3",
|
||||
"connect_messaging_apps": "קישור אפליקציות להעברת הודעות",
|
||||
"connect_crm_apps": "קישור אפליקציות CRM",
|
||||
"current_step_of_total": "שלב {{currentStep}} מתוך {{maxSteps}}",
|
||||
"add_variable": "להוסיף משתנה",
|
||||
"custom_phone_number": "מספר טלפון בהתאמה אישית",
|
||||
@@ -1248,12 +1320,14 @@
|
||||
"location_info": "מיקום האירוע",
|
||||
"additional_notes_info": "הערות נוספות להזמנה",
|
||||
"attendee_name_info": "שם האדם שביצע את ההזמנה",
|
||||
"organizer_name_info": "שם המארגן/ת",
|
||||
"to": "אל",
|
||||
"workflow_turned_on_successfully": "תהליך העבודה {{workflowName}} הועבר למצב {{offOn}} בהצלחה",
|
||||
"download_responses": "הורד תגובות",
|
||||
"download_responses_description": "הורד את כל התגובות לטופס שלך בפורמט CSV.",
|
||||
"download": "הורדה",
|
||||
"download_recording": "הורדת ההקלטה",
|
||||
"recording_from_your_recent_call": "הקלטה של שיחה שערכת לאחרונה ב-{{appName}} מוכנה להורדה",
|
||||
"create_your_first_form": "צור/צרי את הטופס הראשון שלך",
|
||||
"create_your_first_form_description": "באמצעות טפסי ניתוב ניתן לשאול שאלות סיווג ולנתב אל האדם או אל סוג האירוע המתאימים.",
|
||||
"create_your_first_webhook": "יצירת ה-Webhook הראשון שלך",
|
||||
@@ -1262,7 +1336,9 @@
|
||||
"event_one": "אירוע",
|
||||
"event_other": "אירועים",
|
||||
"profile_team_description": "ניהול ההגדרות של פרופיל הצוות",
|
||||
"profile_org_description": "ניהול הגדרות עבור הפרופיל שלך בארגון",
|
||||
"members_team_description": "משתמשים שמשתייכים לקבוצה",
|
||||
"organization_description": "ניהול מנהלי המערכת והחברים בארגון",
|
||||
"team_url": "כתובת ה-URL של הצוות",
|
||||
"team_members": "חברי הצוות",
|
||||
"more": "עוד",
|
||||
@@ -1303,6 +1379,7 @@
|
||||
"routing_forms_send_email_owner": "שליחת דוא\"ל לבעלים",
|
||||
"routing_forms_send_email_owner_description": "דוא\"ל נשלח לבעלים לאחר שליחת הטופס",
|
||||
"add_new_form": "הוספת טופס חדש",
|
||||
"add_new_team_form": "הוספת טופס חדש לצוות",
|
||||
"create_your_first_route": "צור את המסלול הראשון שלך",
|
||||
"route_to_the_right_person": "מסלל לאדם הנכון בהתאם לתשובות בטופס שלך",
|
||||
"form_description": "צור/י טופס משלך כדי לנתב אל מזמין/ה",
|
||||
@@ -1388,6 +1465,7 @@
|
||||
"new_event_type_availability": "זמינות {{eventTypeTitle}}",
|
||||
"error_editing_availability": "אירעה שגיאה במהלך עריכת פרטי הזמינות",
|
||||
"dont_have_permission": "אין לך הרשאת גישה אל המשאב הזה.",
|
||||
"saml_config": "SAML",
|
||||
"saml_configuration_placeholder": "יש להדביק את המטא-נתונים של SAML מספק הזהויות כאן",
|
||||
"saml_email_required": "הזן כתובת דוא\"ל כדי שנוכל למצוא את ספק זהויות SAML שלך",
|
||||
"saml_sp_title": "פרטי ספק השירות",
|
||||
@@ -1406,15 +1484,19 @@
|
||||
"yearly": "אחת לשנה",
|
||||
"checkout": "תשלום",
|
||||
"your_team_disbanded_successfully": "פיזור הצוות שלך בוצע בהצלחה",
|
||||
"your_org_disbanded_successfully": "פיזור הארגון שלך בוצע בהצלחה",
|
||||
"error_creating_team": "אירעה שגיאה במהלך יצירת הצוות",
|
||||
"you": "את/ה",
|
||||
"resend_email": "שליחה חוזרת של הדוא\"ל",
|
||||
"member_already_invited": "החבר כבר הוזמן",
|
||||
"enter_email_or_username": "יש להזין כתובת דוא\"ל או שם משתמש",
|
||||
"team_name_taken": "השם הזה כבר תפוס",
|
||||
"must_enter_team_name": "יש להזין שם צוות",
|
||||
"team_url_required": "יש להזין כתובת URL של הצוות",
|
||||
"url_taken": "כתובת ה-URL הזו כבר תפוסה",
|
||||
"problem_registering_domain": "היתה בעיה ברישום תת-הדומיין; אפשר לנסות שוב או לפנות למנהל/ת מערכת",
|
||||
"team_publish": "פרסום צוות",
|
||||
"number_text_notifications": "מספר טלפון (להודעות טקסט)",
|
||||
"attendee_email_variable": "כתובת הדוא\"ל של המשתתף",
|
||||
"attendee_email_info": "כתובת הדוא\"ל של האדם שביצע את ההזמנה",
|
||||
"kbar_search_placeholder": "הקלד/י פקודה או חפש/י...",
|
||||
@@ -1437,6 +1519,8 @@
|
||||
"find_the_best_person": "למצוא את האדם הכי מתאים שגם זמין לפגישה, ולעבור בסבב דרך כל חברי הצוות.",
|
||||
"fixed_round_robin": "סבב קבוע",
|
||||
"add_one_fixed_attendee": "הוסף/י משתתף/ת קבוע/ה אחד/ת ועבור/י בסבב בין מספר משתתפים.",
|
||||
"calcom_is_better_with_team": "{{appName}} טוב יותר לשימוש עם צוותים",
|
||||
"the_calcom_team": "הצוות של {{companyName}}",
|
||||
"add_your_team_members": "הוסף/י את חברי הצוות לסוגי האירועים שלך. השתמש/י בתזמון שיתופי כדי לכלול את כולם או מצא/י את האדם הכי מתאים בעזרת תזמון בסבב.",
|
||||
"booking_limit_reached": "הגעת להגבלת ההזמנות עבור סוג אירוע זה",
|
||||
"duration_limit_reached": "מגבלת הזמן לארוע זה עבר",
|
||||
@@ -1455,6 +1539,7 @@
|
||||
"navigate_installed_apps": "מעבר לאפליקציות המותקנות",
|
||||
"disabled_calendar": "אם התקנת לוח שנה אחר, ההזמנות החדשות יתווספו אליו. אם לא, קשר/י לוח שנה חדש כדי לא להחמיץ הזמנות חדשות.",
|
||||
"enable_apps": "הפעלת אפליקציות",
|
||||
"enable_apps_description": "הפעל/י אפליקציות שמשתמשים יכולים לשלב עם {{appName}}",
|
||||
"purchase_license": "רכוש רישיון",
|
||||
"already_have_key": "כבר יש לי מפתח:",
|
||||
"already_have_key_suggestion": "אנא העתק את משתנה הסביבה CALCOM_LICENSE_KEY הקיים שלך לכאן.",
|
||||
@@ -1468,6 +1553,9 @@
|
||||
"no_available_apps": "אין אפליקציות זמינות",
|
||||
"no_available_apps_description": "ודא/י שיש אפליקציות בפריסה שלך בנתיב 'packages/app-store'",
|
||||
"no_apps": "אף אפליקציה לא מופעלת במופע הזה של Cal",
|
||||
"no_apps_configured": "עוד לא הוגדרה אף אפליקציה",
|
||||
"enable_in_settings": "ניתן להפעיל אפליקציות ב'הגדרות'",
|
||||
"please_contact_admin": "יש לפנות אל מנהל/ת המערכת",
|
||||
"apps_settings": "הגדרות אפליקציות",
|
||||
"fill_this_field": "נא מלא שדה זה",
|
||||
"options": "אפשרויות",
|
||||
@@ -1571,7 +1659,10 @@
|
||||
"delete_sso_configuration_confirmation_description": "אתה בטוח שאתה רוצה למחוק את הגדרות {{connectionType}}? חברי הצוות שלך שמשתמשים ב- {{connectionType}} להזדהות לא יוכלו להשתמש בו להכנס ל- Cal.com.",
|
||||
"organizer_timezone": "מארגן אזורי זמן",
|
||||
"email_user_cta": "צפה בהזמנה",
|
||||
"email_no_user_invite_heading": "הוזמנת להצטרף ל{{entity}} של {{appName}}",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}} הזמין אותך להצטרף לצוות שלו ב- {{appName}}. {{appName}} הינה מתזמן זימונים שמאפשר לך ולצוות שלך לזמן פגישות בלי כל הפינג פונג במיילים.",
|
||||
"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": "הגדר את הזמינות שלך",
|
||||
@@ -1582,6 +1673,8 @@
|
||||
"scheduler": "{Scheduler}",
|
||||
"no_workflows": "אין תהליכים",
|
||||
"change_filter": "שנה את הסנן כדי לראות תהליכים אישיים וצוותים.",
|
||||
"change_filter_common": "שנה/י את המסנן כדי לראות את התוצאות.",
|
||||
"no_results_for_filter": "אין תוצאות עבור המסנן",
|
||||
"recommended_next_steps": "הצעדים הבאים המומלצים",
|
||||
"create_a_managed_event": "צור ארוע מנוהל",
|
||||
"meetings_are_better_with_the_right": "פגישות טובות יותר עם אנשי הצוות הנכונים נמצאים בו. הזמן אותם עכשיו.",
|
||||
@@ -1592,7 +1685,9 @@
|
||||
"no_longer_attending": "אתה לא משתתף יותר בארוע זה",
|
||||
"attendee_no_longer_attending_subject": "משתתף מסויים אינו משתתף עוד ב- {{title}} במועד {{date}}",
|
||||
"attendee_no_longer_attending": "משתתף מסויים אינו משתתף עוד בארוע שלך",
|
||||
"attendee_no_longer_attending_subtitle": "{{name}} ביטל/ה, ומשמעות הדבר היא שהתפנה מקום בחלון הזמן הזה",
|
||||
"create_event_on": "צור ארוע ב-",
|
||||
"create_routing_form_on": "יצירת טופס ניתוב ב-",
|
||||
"default_app_link_title": "צור קישור אפליקציה ברירת מחדל",
|
||||
"default_app_link_description": "הגדרת קישור אפליקציה ברירת מחדל מאפשר לכל הארועים החדשים להשתמש בקישור שהגדרת.",
|
||||
"organizer_default_conferencing_app": "אפליקציית ברירת המחדל של המארגן/ת",
|
||||
@@ -1602,6 +1697,7 @@
|
||||
"booking_questions_title": "שאלות תזמון",
|
||||
"booking_questions_description": "התאמת השאלות שנשאלות בדף התזמון",
|
||||
"add_a_booking_question": "הוסף שאלה",
|
||||
"identifier": "מזהה",
|
||||
"duplicate_email": "מייל כבר קיים",
|
||||
"booking_with_payment_cancelled": "תשלום עבור ארוע זה כבר אינו נתמך",
|
||||
"booking_with_payment_cancelled_already_paid": "זיכוי עבור התשלום של תזמון זה כבר בדרך.",
|
||||
@@ -1612,6 +1708,7 @@
|
||||
"show_on_booking_page": "להציג בדף ההזמנות",
|
||||
"get_started_zapier_templates": "התחל עם תבניות Zapier",
|
||||
"team_is_unpublished": "צוות {{team}} אינו מפורסם",
|
||||
"team_is_unpublished_description": "קישור ה-{{entity}} הזה אינו זמין כעת. יש ליצור קשר עם הבעלים של ה-{{entity}} או לבקש מהם לפרסם אותו.",
|
||||
"team_member": "חבר צוות",
|
||||
"a_routing_form": "טופס ניתוב",
|
||||
"form_description_placeholder": "תיאור הטופס",
|
||||
@@ -1641,13 +1738,16 @@
|
||||
"spot_popular_event_types": "זהה ארועים פופולרים",
|
||||
"spot_popular_event_types_description": "צפה איזה ארועים מקבלים הכי הרבה גישות ותזמונים",
|
||||
"no_responses_yet": "אין עדיין תגובות",
|
||||
"no_routes_defined": "לא הוגדר אף נתיב",
|
||||
"this_will_be_the_placeholder": "זה יהיה הממלא מקום",
|
||||
"error_booking_event": "אירעה שגיאה בעת הזמנת האירוע, רענן/י את הדף ונסה/י שוב",
|
||||
"timeslot_missing_title": "לא נבחר חלון זמן",
|
||||
"timeslot_missing_description": "כדי להזמין את האירוע, יש לבחור חלון זמן.",
|
||||
"timeslot_missing_cta": "בחר/י חלון זמן",
|
||||
"switch_monthly": "מעבר לתצוגת חודש",
|
||||
"switch_weekly": "מעבר לתצוגת שבוע",
|
||||
"switch_multiday": "מעבר לתצוגת יום",
|
||||
"switch_columnview": "מעבר לתצוגת עמודות",
|
||||
"num_locations": "{{num}} אפשרויות מיקום",
|
||||
"select_on_next_step": "לבחור בשלב הבא",
|
||||
"this_meeting_has_not_started_yet": "פגישה זו עוד לא התחילה",
|
||||
@@ -1686,6 +1786,7 @@
|
||||
"managed_event_dialog_title_one": "כתובת ה-URL /{{slug}} כבר קיימת עבור חבר/ה {{count}}. האם ברצונך להחליף אותה?",
|
||||
"managed_event_dialog_title_other": "כתובת ה-URL /{{slug}} כבר קיימת עבור {{count}} חברים. האם ברצונך להחליף אותה?",
|
||||
"managed_event_dialog_information_one": "<strong>{{names}}</strong> כבר משתמש/ת בכתובת ה-URL <strong>/{{slug}}</strong>.",
|
||||
"managed_event_dialog_information_other": "<strong>{{names}}</strong> כבר משתמשים בכתובת ה-URL <strong>/{{slug}}</strong>.",
|
||||
"managed_event_dialog_clarification": "אם תבחר/י להחליף את כתובת ה-URL, אנו נודיע על כך לחברים. אם אינך רוצה להחליף אותה, חזור/י והסר/י את החברים.",
|
||||
"review_event_type": "בדיקת סוג האירוע",
|
||||
"looking_for_more_analytics": "מחפש עוד מידע אנליטי?",
|
||||
@@ -1701,12 +1802,14 @@
|
||||
"least_booked_members": "החברים שמתוזמנים הכי פחות",
|
||||
"events_created": "ארועים שנוצרו",
|
||||
"events_completed": "ארועים שהושלמו",
|
||||
"events_cancelled": "אירועים שבוטלו",
|
||||
"events_rescheduled": "ארועים שתוזמנו מחדש",
|
||||
"from_last_period": "מפרק הזמן האחרון",
|
||||
"from_to_date_period": "מ: {{startDate}} עד: {{endDate}}",
|
||||
"redirect_url_warning": "הוספת כתובת URL להפניה אוטומטית תגרום להשבתת הדף \"הפעולה הצליחה\". חשוב להוסיף את ההודעה \"ההזמנה אושרה בהצלחה'\"בדף המותאם אישית לציון שהפעולה הצליחה.",
|
||||
"event_trends": "מגמות ארוע",
|
||||
"clear_filters": "ניקוי המסננים",
|
||||
"clear": "נקה",
|
||||
"hold": "החזקה",
|
||||
"on_booking_option": "גביית תשלום בעת ההזמנה",
|
||||
"hold_option": "חיוב דמי אי-הגעה",
|
||||
@@ -1733,6 +1836,18 @@
|
||||
"complete_your_booking": "יש להשלים את ההזמנה",
|
||||
"complete_your_booking_subject": "יש להשלים את ההזמנה: {{title}} ב-{{date}}",
|
||||
"confirm_your_details": "אישור הפרטים שלך",
|
||||
"copy_invite_link": "העתקת קישור ההזמנה",
|
||||
"edit_invite_link": "עריכת הגדרות הקישור",
|
||||
"invite_link_copied": "קישור ההזמנה הועתק",
|
||||
"invite_link_deleted": "קישור ההזמנה נמחק",
|
||||
"invite_link_updated": "הגדרות קישור ההזמנה נשמרו",
|
||||
"link_expires_after": "הקישורים מוגדרים לפוג לאחר...",
|
||||
"one_day": "יום אחד",
|
||||
"seven_days": "7 ימים",
|
||||
"thirty_days": "30 ימים",
|
||||
"three_months": "3 חודשים",
|
||||
"one_year": "שנה אחת",
|
||||
"team_invite_received": "הוזמנת להצטרף אל {{teamName}}",
|
||||
"currency_string": "{{amount, currency}}",
|
||||
"charge_card_dialog_body": "את/ה עומד/ת לחייב את המשתתף/ת בסכום של {{amount, currency}}. בטוח/ה שברצונך להמשיך?",
|
||||
"charge_attendee": "לחייב את המשתתף/ת ב-{{amount, currency}}",
|
||||
@@ -1741,6 +1856,7 @@
|
||||
"email_invite_team_bulk": "{{userCount}} משתמשים הוזמנו",
|
||||
"error_collecting_card": "שגיאה בחיוב הכרטיס",
|
||||
"image_size_limit_exceed": "גודל התמונה המרבי שניתן להעלות הוא 5MB",
|
||||
"unauthorized_workflow_error_message": "{{errorCode}}: אין לך הרשאה להפעיל או להשבית את תהליך העבודה הזה",
|
||||
"inline_embed": "הטמעה באתר",
|
||||
"load_inline_content": "סוג האירוע נטען ישירות בתוך שאר תוכן האתר שלך.",
|
||||
"floating_pop_up_button": "כפתור קופץ צף",
|
||||
@@ -1760,6 +1876,69 @@
|
||||
"connect_google_workspace": "חיבור Google Workspace",
|
||||
"google_workspace_admin_tooltip": "עליך/ייך להיות אדמין/ית ב-Workspace כדי להשתמש בתכונה הזו",
|
||||
"first_event_type_webhook_description": "צור/צרי את ה-Webhook הראשון שלך עבור סוג האירוע הזה",
|
||||
"install_app_on": "התקנת האפליקציה ב-",
|
||||
"create_for": "צור/צרי עבור",
|
||||
"insights_all_org_filter": "כל האפליקציות"
|
||||
"setup_organization": "הגדרת ארגון",
|
||||
"organization_banner_description": "צור/י סביבות שבהן הצוותים שלך יכולים ליצור אפליקציות, תהליכי עבודה וסוגי אירועים משותפים, עם תכונות כמו סבב וקביעת מועדים שיתופית.",
|
||||
"organization_banner_title": "ניהול ארגונים עם צוותים מרובים",
|
||||
"set_up_your_organization": "הגדרת הארגון",
|
||||
"organizations_description": "ארגונים הם סביבות משותפות שבהן צוותים יכולים ליצור משאבים משותפים, כמו סוגי אירועים, אפליקציות, תהליכי עבודה ועוד.",
|
||||
"must_enter_organization_name": "יש להזין את שם הארגון",
|
||||
"must_enter_organization_admin_email": "יש להזין את כתובת הדוא\"ל של הארגון",
|
||||
"admin_email": "כתובת הדוא\"ל של הארגון",
|
||||
"admin_username": "שם המשתמש/ת המוגדר/ת כמנהל/ת מערכת",
|
||||
"organization_name": "שם הארגון",
|
||||
"organization_url": "כתובת ה-URL של הארגון",
|
||||
"organization_verify_header": "אימות כתובת הדוא\"ל של הארגון",
|
||||
"organization_verify_email_body": "השתמש/י בקוד הבא כדי לאמת את כתובת הדוא\"ל שלך לצורך המשך הגדרת הארגון.",
|
||||
"additional_url_parameters": "פרמטרי URL נוספים",
|
||||
"about_your_organization": "מידע על הארגון",
|
||||
"about_your_organization_description": "ארגונים הם סביבות משותפות שבהן ניתן ליצור צוותים מרובים עם משאבים משותפים, כמו חברים, סוגי אירועים, אפליקציות, תהליכי עבודה ועוד.",
|
||||
"create_your_teams": "יצירת הצוותים שלך",
|
||||
"create_your_teams_description": "מוסיפים את חברי הצוות לארגון ומתחילים לקבוע מועדים לפגישות ביחד",
|
||||
"invite_organization_admins": "הזמנת מנהלי המערכת בארגון",
|
||||
"invite_organization_admins_description": "למנהלי המערכת האלה תהיה גישה לכל הצוותים בארגון. ניתן להוסיף לצוותים מנהלי מערכת וחברים מאוחר יותר.",
|
||||
"set_a_password": "הגדרת סיסמה",
|
||||
"set_a_password_description": "פעולה זו תיצור חשבון משתמש חדש עם כתובת הדוא\"ל של הארגון ועם הסיסמה הזו.",
|
||||
"organization_logo": "הסמל של הארגון",
|
||||
"organization_about_description": "מספר משפטים אודות הארגון. הם יופיעו בדף הפרופיל הציבורי של הארגון.",
|
||||
"ill_do_this_later": "אעשה זאת מאוחר יותר",
|
||||
"verify_your_email": "אימות כתובת הדוא״ל",
|
||||
"enter_digit_code": "הזן/י את הקוד בן 6 הספרות ששלחנו לכתובת {{email}}",
|
||||
"verify_email_organization": "אמת/י את כתובת הדוא\"ל שלך כדי ליצור ארגון",
|
||||
"code_provided_invalid": "הקוד לא תקין, נסה/י שוב",
|
||||
"email_already_used": "כתובת הדוא\"ל הזו כבר בשימוש",
|
||||
"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_your_profile_description": "אפשר/י לאנשים לדעת מי את/ה ב-{{orgName}} וכשהם מקיימים איתך אינטראקציה באמצעות הקישור הציבורי שלך.",
|
||||
"my_profile": "הפרופיל שלי",
|
||||
"my_settings": "ההגדרות שלי",
|
||||
"crm": "CRM",
|
||||
"messaging": "העברת הודעות",
|
||||
"sender_id_info": "השם או המספר שמוצגים כשולח של הודעת SMS (חלק מהמדינות אינן מתירות שימוש במזהי שולח אלפאנומריים)",
|
||||
"org_admins_can_create_new_teams": "רק מנהל/ת המערכת בארגון יכול/ה ליצור צוותים חדשים",
|
||||
"google_new_spam_policy": "מדיניות הספאם החדשה של Google עלולה לגרום לך שלא תקבל/י עדכונים בדוא\"ל ובלוח השנה לגבי הפגישה הזו.",
|
||||
"resolve": "פתרון",
|
||||
"no_organization_slug": "היתה שגיאה במהלך ניסיון ליצור צוותים עבור הארגון הזה. חסר רכיב slug של URL.",
|
||||
"org_name": "שם הארגון",
|
||||
"org_url": "כתובת ה-URL של הארגון",
|
||||
"copy_link_org": "העתקת הקישור לארגון",
|
||||
"404_the_org": "הארגון",
|
||||
"404_the_team": "הצוות",
|
||||
"404_claim_entity_org": "תבע/י את תת-הדומיין עבור הארגון שלך",
|
||||
"404_claim_entity_team": "תבע/י את הצוות הזה והתחל/י לנהל קביעת מועדים ביחד",
|
||||
"insights_all_org_filter": "כל האפליקציות",
|
||||
"insights_team_filter": "צוות: {{teamName}}",
|
||||
"insights_user_filter": "משתמש: {{userName}}",
|
||||
"insights_subtitle": "הצגת תובנות הזמנה מתוך האירועים שלך",
|
||||
"custom_plan": "חבילה בהתאמה אישית",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -208,5 +208,6 @@
|
||||
"web_conference": "Online konferencia",
|
||||
"timezone_variable": "Időzóna",
|
||||
"location_variable": "Helyszín",
|
||||
"additional_notes_variable": "Egyéb jegyzetek",
|
||||
"insights_all_org_filter": "Minden alkalmazás"
|
||||
}
|
||||
|
||||
@@ -1178,6 +1178,7 @@
|
||||
"event_time_variable": "Ora evento",
|
||||
"timezone_variable": "Timezone",
|
||||
"location_variable": "Luogo",
|
||||
"additional_notes_variable": "Note aggiuntive",
|
||||
"organizer_name_variable": "Nome organizzatore",
|
||||
"app_upgrade_description": "Per poter utilizzare questa funzionalità, è necessario passare a un account Pro.",
|
||||
"invalid_number": "Numero di telefono non valido",
|
||||
|
||||
@@ -1178,6 +1178,7 @@
|
||||
"event_time_variable": "イベントの時間",
|
||||
"timezone_variable": "タイムゾーン",
|
||||
"location_variable": "場所",
|
||||
"additional_notes_variable": "備考",
|
||||
"organizer_name_variable": "主催者名",
|
||||
"app_upgrade_description": "この機能を利用するには、Pro アカウントへのアップグレードが必要です。",
|
||||
"invalid_number": "電話番号が無効です",
|
||||
|
||||
@@ -1178,6 +1178,7 @@
|
||||
"event_time_variable": "이벤트 시간",
|
||||
"timezone_variable": "시간대",
|
||||
"location_variable": "위치",
|
||||
"additional_notes_variable": "추가 참고 사항",
|
||||
"organizer_name_variable": "주최자 이름",
|
||||
"app_upgrade_description": "이 기능을 사용하려면 Pro 계정으로 업그레이드해야 합니다.",
|
||||
"invalid_number": "유효하지 않은 전화 번호",
|
||||
|
||||
@@ -1178,6 +1178,7 @@
|
||||
"event_time_variable": "Tijd gebeurtenis",
|
||||
"timezone_variable": "Tijdzone",
|
||||
"location_variable": "Locatie",
|
||||
"additional_notes_variable": "Aanvullende opmerkingen",
|
||||
"organizer_name_variable": "Naam organisator",
|
||||
"app_upgrade_description": "Om deze functie te gebruiken, moet u upgraden naar een Pro-account.",
|
||||
"invalid_number": "Ongeldig telefoonnummer",
|
||||
|
||||
@@ -1036,6 +1036,7 @@
|
||||
"event_time_variable": "Tidspunkt for hendelse",
|
||||
"timezone_variable": "Tidssone",
|
||||
"location_variable": "Sted",
|
||||
"additional_notes_variable": "Tilleggsinformasjon",
|
||||
"app_upgrade_description": "For å bruke denne funksjonen må du oppgradere til en Pro-konto.",
|
||||
"invalid_number": "Ugyldig telefonnummer",
|
||||
"navigate": "Naviger",
|
||||
|
||||
@@ -1178,6 +1178,7 @@
|
||||
"event_time_variable": "Godzina wydarzenia",
|
||||
"timezone_variable": "Strefa Czasowa",
|
||||
"location_variable": "Lokalizacja",
|
||||
"additional_notes_variable": "Dodatkowe uwagi",
|
||||
"organizer_name_variable": "Nazwa organizatora",
|
||||
"app_upgrade_description": "Aby korzystać z tej funkcji, musisz uaktualnić do konta Pro.",
|
||||
"invalid_number": "Nieprawidłowy numer telefonu",
|
||||
|
||||
@@ -1178,6 +1178,7 @@
|
||||
"event_time_variable": "Horário do evento",
|
||||
"timezone_variable": "Fuso Horário",
|
||||
"location_variable": "Local",
|
||||
"additional_notes_variable": "Observações adicionais",
|
||||
"organizer_name_variable": "Nome do organizador",
|
||||
"app_upgrade_description": "Para usar este recurso, atualize para uma conta Pro.",
|
||||
"invalid_number": "Número de telefone inválido",
|
||||
|
||||
@@ -1178,6 +1178,7 @@
|
||||
"event_time_variable": "Hora do evento",
|
||||
"timezone_variable": "Fuso Horário",
|
||||
"location_variable": "Localização",
|
||||
"additional_notes_variable": "Notas Adicionais",
|
||||
"organizer_name_variable": "Nome do organizador",
|
||||
"app_upgrade_description": "Para usar esta funcionalidade, tem de actualizar para uma conta Pro.",
|
||||
"invalid_number": "Número de telefone inválido",
|
||||
|
||||
@@ -1178,6 +1178,7 @@
|
||||
"event_time_variable": "Oră eveniment",
|
||||
"timezone_variable": "Timezone",
|
||||
"location_variable": "Loc",
|
||||
"additional_notes_variable": "Note suplimentare",
|
||||
"organizer_name_variable": "Nume organizator",
|
||||
"app_upgrade_description": "Pentru a utiliza această caracteristică, trebuie să faceți upgrade la un cont Pro.",
|
||||
"invalid_number": "Număr de telefon nevalid",
|
||||
|
||||
@@ -1178,6 +1178,7 @@
|
||||
"event_time_variable": "Время события",
|
||||
"timezone_variable": "Часовой пояс",
|
||||
"location_variable": "Местоположение",
|
||||
"additional_notes_variable": "Дополнительная информация",
|
||||
"organizer_name_variable": "Имя организатора",
|
||||
"app_upgrade_description": "Чтобы использовать эту функцию, необходимо перейти на аккаунт Pro.",
|
||||
"invalid_number": "Неверный номер телефона",
|
||||
|
||||
@@ -1 +1,32 @@
|
||||
{}
|
||||
{
|
||||
"identity_provider": "Poskytovateľ identity",
|
||||
"trial_days_left": "V PRO verzii na skúšku vám zostávajú $t(day, {\"count\": {{days}} })",
|
||||
"day_one": "{{count}} deň",
|
||||
"day_other": "{{count}} dni",
|
||||
"second_one": "{{count}} sekunda",
|
||||
"second_other": "{{count}} sekundy",
|
||||
"upgrade_now": "Prejsť na platený program",
|
||||
"accept_invitation": "Prijať pozvánku",
|
||||
"calcom_explained": "{{appName}} poskytuje plánovaciu infraštruktúru pre každého.",
|
||||
"calcom_explained_new_user": "Dokončite nastavenie svojho {{appName}} účtu! Ste už iba niekoľko krokov od vyriešenia všetkých svojich problémov s plánovaním.",
|
||||
"have_any_questions": "Máte otázky? Sme tu, aby sme vám pomohli.",
|
||||
"reset_password_subject": "{{appName}}: Pokyny pre obnovenie hesla",
|
||||
"verify_email_subject": "{{appName}}: Overte svoj účet",
|
||||
"check_your_email": "Skontrolujte svoj e-mail",
|
||||
"verify_email_page_body": "Odoslali sme e-mail na adresu {{email}}. E-mailovú adresu je potrebné overiť, aby bola zaručená čo najlepšia doručiteľnosť e-mailov a kalendárov z {{appName}}.",
|
||||
"verify_email_banner_body": "Overte svoju e-mailovú adresu, aby ste zaručili čo najlepšiu doručiteľnosť e-mailov a kalendára",
|
||||
"verify_email_email_header": "Overte svoju e-mailovú adresu",
|
||||
"verify_email_email_button": "Oveřiť e-mail",
|
||||
"copy_somewhere_safe": "Uložte tento kľúč API niekde v bezpečí. Nebudete ho môcť znova zobraziť.",
|
||||
"verify_email_email_body": "Overte svoju e-mailovú adresu kliknutím na tlačidlo nižšie.",
|
||||
"verify_email_email_link_text": "Tu je odkaz pre prípad, že nemáte radi klikanie na tlačidlá:",
|
||||
"email_sent": "E-mail bol odoslaný",
|
||||
"event_declined_subject": "Zamietnuté: {{title}} v {{date}}",
|
||||
"event_cancelled_subject": "Zrušené: {{title}} ({{date}})",
|
||||
"event_request_declined": "Žiadosť o udalosť bola zamietnutá",
|
||||
"event_request_declined_recurring": "Žiadosť o opakovanú udalosť bola zamietnutá",
|
||||
"event_request_cancelled": "Vaša plánovaná udalosť bola zrušená",
|
||||
"organizer": "Organizátor",
|
||||
"need_to_reschedule_or_cancel": "Preplánovať alebo zrušiť?",
|
||||
"no_options_available": "Nie sú k dispozícii žiadne možnosti"
|
||||
}
|
||||
|
||||
@@ -1179,6 +1179,7 @@
|
||||
"event_time_variable": "Vreme događaja",
|
||||
"timezone_variable": "Vremenska zona",
|
||||
"location_variable": "Lokacija",
|
||||
"additional_notes_variable": "Dodatne beleške",
|
||||
"organizer_name_variable": "Ime organizatora",
|
||||
"app_upgrade_description": "Da biste koristili ovu funkciju, morate izvršiti nadogradnju na Pro nalog.",
|
||||
"invalid_number": "Neispravan broj telefona",
|
||||
|
||||
@@ -1178,6 +1178,7 @@
|
||||
"event_time_variable": "Händelsetid",
|
||||
"timezone_variable": "Tidszon",
|
||||
"location_variable": "Plats",
|
||||
"additional_notes_variable": "Ytterligare inmatning",
|
||||
"organizer_name_variable": "Arrangörens namn",
|
||||
"app_upgrade_description": "För att kunna använda den här funktionen måste du uppgradera till ett Pro-konto.",
|
||||
"invalid_number": "Ogiltigt telefonnummer",
|
||||
|
||||
@@ -1178,6 +1178,7 @@
|
||||
"event_time_variable": "Etkinlik saati",
|
||||
"timezone_variable": "Saat dilimi",
|
||||
"location_variable": "Konum",
|
||||
"additional_notes_variable": "Ek notlar",
|
||||
"organizer_name_variable": "Düzenleyenin adı",
|
||||
"app_upgrade_description": "Bu özelliği kullanmak için Pro hesabına geçmeniz gerekiyor.",
|
||||
"invalid_number": "Geçersiz telefon numarası",
|
||||
|
||||
@@ -1178,6 +1178,7 @@
|
||||
"event_time_variable": "Час заходу",
|
||||
"timezone_variable": "Часовий пояс",
|
||||
"location_variable": "Розташування",
|
||||
"additional_notes_variable": "Додаткові примітки",
|
||||
"organizer_name_variable": "Ім’я організатора",
|
||||
"app_upgrade_description": "Щоб користуватися цією функцією, потрібен обліковий запис Pro.",
|
||||
"invalid_number": "Недійсний номер телефону",
|
||||
|
||||
@@ -1178,6 +1178,7 @@
|
||||
"event_time_variable": "Thời gian sự kiện",
|
||||
"timezone_variable": "Múi giờ",
|
||||
"location_variable": "Vị trí",
|
||||
"additional_notes_variable": "Ghi chú bổ sung",
|
||||
"organizer_name_variable": "Tên nhà tổ chức",
|
||||
"app_upgrade_description": "Để sử dụng tính năng này, bạn cần nâng cấp lên tài khoản Pro.",
|
||||
"invalid_number": "Số điện thoại không hợp lệ",
|
||||
@@ -1895,6 +1896,49 @@
|
||||
"about_your_organization_description": "Tổ chức là môi trường chia sẻ tại đó bạn có thể tạo nhiều nhóm có thành viên chung, loại sự kiện chung, ứng dụng chung, tiến độ công việc chung, v.v..",
|
||||
"create_your_teams": "Tạo nhóm của bạn",
|
||||
"create_your_teams_description": "Bắt đầu đặt lịch cùng nhau bằng cách thêm thành viên nhóm bạn vào tổ chức",
|
||||
"invite_organization_admins": "Mời những quản trị viên của tổ chức",
|
||||
"invite_organization_admins_description": "Những quản trị viên này có quyền truy cập mọi nhóm trong tổ chức. Bạn có thể thêm quản trị viên và thành viên của nhóm sau đó.",
|
||||
"set_a_password": "Đặt mật khẩu",
|
||||
"set_a_password_description": "Thao tác này sẽ tạo một tài khoản người dùng mới với email của tổ chức và mật khẩu này.",
|
||||
"organization_logo": "Logo của tổ chức",
|
||||
"organization_about_description": "Một vài câu về tổ chức của bạn. Thông tin này sẽ xuất hiện trên trang hồ sơ công cộng của tổ chức.",
|
||||
"ill_do_this_later": "Tôi sẽ làm sau",
|
||||
"verify_your_email": "Xác minh email của bạn",
|
||||
"enter_digit_code": "Nhập mã 6 chữ số mà chúng tôi đã gửi tới {{email}}",
|
||||
"verify_email_organization": "Xác minh email của bạn để tạo một tổ chức",
|
||||
"code_provided_invalid": "Mã cung cấp là mã không hợp lệ, hãy thử lại",
|
||||
"email_already_used": "Email đã được dùng",
|
||||
"organization_admin_invited_heading": "Bạn đã được mời để gia nhập {{orgName}}",
|
||||
"organization_admin_invited_body": "Gia nhập nhóm của bạn tại {{orgName}} và bắt đầu tập trung vào cuộc họp, chứ không còn lo tạo ra cuộc họp!",
|
||||
"duplicated_slugs_warning": "Những nhóm sau không thể tạo do có slug trùng lặp: {{slugs}}",
|
||||
"team_names_empty": "Tên nhóm không được để trống",
|
||||
"team_names_repeated": "Tên nhóm không được lặp lại",
|
||||
"user_belongs_organization": "Người dùng thuộc về một nhóm",
|
||||
"org_no_teams_yet": "Tổ chức này chưa có nhóm nào",
|
||||
"org_no_teams_yet_description": "Nếu bạn là một quản trị viên, hãy bảo đảm tạo nhóm các nhóm cần được hiển thị tại đây.",
|
||||
"set_up": "Thiết lập",
|
||||
"set_up_your_profile": "Thiết lập hồ sơ của bạn",
|
||||
"set_up_your_profile_description": "Hãy để mọi người biết bạn là ai trong {{orgName}}, và họ biết khi nhấp vào liên kết công cộng của bạn.",
|
||||
"my_profile": "Hồ sơ của tôi",
|
||||
"my_settings": "Cài đặt của tôi",
|
||||
"crm": "CRM",
|
||||
"messaging": "Nhắn tin",
|
||||
"sender_id_info": "Tên hoặc số hiển thị ở chỗ người gửi SMS (một số quốc gia không cho phép ID người gửi dạng chữ lẫn số)",
|
||||
"org_admins_can_create_new_teams": "Chỉ quản trị viên của tổ chức mới tạo được nhóm mới",
|
||||
"google_new_spam_policy": "Chính sách spam mới của Google có thể ngăn bạn không cho nhận bất kì thông báo nào về email và lịch liên quan đến lịch hẹn này.",
|
||||
"resolve": "Giải quyết",
|
||||
"no_organization_slug": "Có lỗi khi tạo nhóm cho tổ chức này. Slug URL bị thiếu.",
|
||||
"org_name": "Tên tổ chức",
|
||||
"org_url": "URL của tổ chức",
|
||||
"copy_link_org": "Sao chép liên kết đến tổ chức",
|
||||
"404_the_org": "Tổ chức",
|
||||
"404_the_team": "Nhóm",
|
||||
"404_claim_entity_org": "Lấy tên miền phụ cho tổ chức của bạn",
|
||||
"404_claim_entity_team": "Lấy nhóm này và bắt đầu quản lý lịch hẹn theo tập thể",
|
||||
"insights_all_org_filter": "Tất cả"
|
||||
"insights_all_org_filter": "Tất cả",
|
||||
"insights_team_filter": "Nhóm: {{teamName}}",
|
||||
"insights_user_filter": "Người dùng: {{userName}}",
|
||||
"insights_subtitle": "Xem số liệu của lịch hẹn ở mọi sự kiện của bạn",
|
||||
"custom_plan": "Gói Tuỳ chỉnh",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"verify_email_banner_body": "验证您的电子邮件地址以确保最佳的电子邮件和日历递送能力",
|
||||
"verify_email_email_header": "验证您的电子邮件地址",
|
||||
"verify_email_email_button": "验证电子邮件",
|
||||
"copy_somewhere_safe": "将此 API 密钥保存在安全的地方。您将无法再次查看它。",
|
||||
"verify_email_email_body": "请点击下面的按钮来验证您的电子邮件地址。",
|
||||
"verify_email_email_link_text": "如果您不喜欢点击按钮,请点击以下链接:",
|
||||
"email_sent": "电子邮件已成功发送",
|
||||
@@ -243,6 +244,8 @@
|
||||
"all": "所有应用",
|
||||
"yours": "您的账户",
|
||||
"available_apps": "可用应用",
|
||||
"available_apps_lower_case": "可用的应用",
|
||||
"available_apps_desc": "您尚未安装任何应用。在我们的<1>应用商店</1>中查看热门应用,并探索更多内容。",
|
||||
"check_email_reset_password": "请检查您的邮箱,我们向您发送了一个重置密码的链接。",
|
||||
"finish": "完成",
|
||||
"organization_general_description": "管理您的团队语言和时区的设置",
|
||||
@@ -349,7 +352,7 @@
|
||||
"no_meeting_found": "未找到会议",
|
||||
"no_meeting_found_description": "此会议不存在。联系会议所有者获取更新后的链接。",
|
||||
"no_status_bookings_yet": "暂无 {{status}} 预约",
|
||||
"no_status_bookings_yet_description": "您没有{{status}}预约,{{description}}",
|
||||
"no_status_bookings_yet_description": "您没有 {{status}} 预约,{{description}}",
|
||||
"event_between_users": "{{host}} 和 {{attendeeName}} 之间的 {{eventName}}",
|
||||
"bookings": "预约",
|
||||
"booking_not_found": "找不到预约",
|
||||
@@ -399,35 +402,36 @@
|
||||
"leverage_our_api": "利用我们的 API 来实现完全控制和高度定制。",
|
||||
"create_webhook": "创建 Webhook",
|
||||
"booking_cancelled": "预约已取消",
|
||||
"booking_rescheduled": "预约已重新安排",
|
||||
"booking_rescheduled": "预约已改期",
|
||||
"recording_ready": "录制内容下载链接就绪",
|
||||
"booking_created": "预约已创建",
|
||||
"booking_rejected": "预约被拒绝",
|
||||
"booking_requested": "预约已请求",
|
||||
"meeting_ended": "会议已结束",
|
||||
"form_submitted": "表格已提交",
|
||||
"booking_paid": "预约已付款",
|
||||
"event_triggers": "事件触发",
|
||||
"subscriber_url": "订阅者链接",
|
||||
"create_new_webhook": "创建一个新的 webhook",
|
||||
"create_new_webhook": "创建一个新的 Webhook",
|
||||
"webhooks": "Webhook",
|
||||
"team_webhooks": "团队 Webhook",
|
||||
"create_new_webhook_to_account": "为您的账户创建一个新的Webhook",
|
||||
"create_new_webhook_to_account": "为您的账户创建一个新的 Webhook",
|
||||
"new_webhook": "新建 Webhook",
|
||||
"receive_cal_meeting_data": "当安排或取消活动后,在指定链接实时接收 {{appName}} 会议数据。",
|
||||
"receive_cal_event_meeting_data": "当安排或取消此活动后,在指定链接实时接收 {{appName}} 会议数据。",
|
||||
"responsive_fullscreen_iframe": "全屏响应式iframe",
|
||||
"responsive_fullscreen_iframe": "全屏响应式 iframe",
|
||||
"loading": "加载中...",
|
||||
"deleting": "正在删除...",
|
||||
"standard_iframe": "标准 iframe",
|
||||
"developer": "开发人员",
|
||||
"manage_developer_settings": "管理您的开发人员设置。",
|
||||
"iframe_embed": "嵌入 iframe",
|
||||
"embed_calcom": "用最简单的方式将{{appName}}嵌入您的网站",
|
||||
"embed_calcom": "用最简单的方式将 {{appName}} 嵌入您的网站",
|
||||
"integrate_using_embed_or_webhooks": "将我们的功能嵌入式集成进您的网站,或者使用自定义Webhook获得实时预约信息。",
|
||||
"schedule_a_meeting": "安排会议",
|
||||
"view_and_manage_billing_details": "查看和管理您的账单详情",
|
||||
"view_and_edit_billing_details": "查看和编辑您的账单详情,以及取消订阅。",
|
||||
"go_to_billing_portal": "转到账单门户网站",
|
||||
"go_to_billing_portal": "前往付费入口",
|
||||
"need_anything_else": "还有其他需要?",
|
||||
"further_billing_help": "如果您在支付帐单时需要任何进一步的帮助,我们的支持团队会给您提供帮助。",
|
||||
"contact": "联系",
|
||||
@@ -706,8 +710,8 @@
|
||||
"upgrade_to_flexible_pro_message": "您的团队中有没有位置的成员。升级到专业版来获得更多位置。",
|
||||
"changed_team_billing_info": "自2022年1月起,我们将按每个位置向团队成员收取费用。 您的团队中免费获得专业版的成员现在正在进行为期14天的试用。 试用期结束后,这些成员将在您的团队中隐藏,除非您现在升级到专业版。",
|
||||
"create_manage_teams_collaborative": "创建并管理团队以使用协作功能。",
|
||||
"only_available_on_pro_plan": "此功能仅在Pro套餐中可用",
|
||||
"remove_cal_branding_description": "若想从您的预约页面中去掉{{appName}}品牌标志,您需要升级到专业版账户。",
|
||||
"only_available_on_pro_plan": "此功能仅在 Pro 套餐中可用",
|
||||
"remove_cal_branding_description": "若想从您的预约页面中去掉 {{appName}} 的品牌标志,您需要升级到专业版账户。",
|
||||
"edit_profile_info_description": "编辑您的个人资料,在您的预约链接上显示。",
|
||||
"change_email_tip": "您可能需要注销并重新登录才能使更改生效。",
|
||||
"little_something_about": "一些关于你自己的描述。",
|
||||
@@ -767,12 +771,12 @@
|
||||
"set_link_meeting": "设置会议链接",
|
||||
"cal_invitee_phone_number_scheduling": "{{appName}} 将要求您的受邀者在安排时间之前提供电话号码。",
|
||||
"cal_provide_google_meet_location": "{{appName}} 将提供 Google Meet 位置。",
|
||||
"cal_provide_zoom_meeting_url": "{{appName}}将提供Zoom会议链接",
|
||||
"cal_provide_tandem_meeting_url": "{{appName}} 将提供Tandem会议链接",
|
||||
"cal_provide_video_meeting_url": "{{appName}}将提供Daily视频会议链接",
|
||||
"cal_provide_jitsi_meeting_url": "{{appName}} 将提供Jitsi Meet视频会议链接",
|
||||
"cal_provide_huddle01_meeting_url": "{{appName}} 将提供Huddle01 Web3视频会议链接",
|
||||
"cal_provide_teams_meeting_url": "{{appName}} 将提供 MS Teams 会议 URL。注意: 必须拥有工作或学校帐户",
|
||||
"cal_provide_zoom_meeting_url": "{{appName}}将提供 Zoom 会议链接",
|
||||
"cal_provide_tandem_meeting_url": "{{appName}} 将提供 Tandem 会议链接",
|
||||
"cal_provide_video_meeting_url": "{{appName}}将提供 Daily 视频会议链接",
|
||||
"cal_provide_jitsi_meeting_url": "{{appName}} 将提供 Jitsi Meet 视频会议链接",
|
||||
"cal_provide_huddle01_meeting_url": "{{appName}} 将提供 Huddle01 web3 视频会议链接",
|
||||
"cal_provide_teams_meeting_url": "{{appName}} 将提供 MS Teams 会议 URL。注意: 须拥有工作或学校帐户",
|
||||
"require_payment": "需要付款",
|
||||
"you_need_to_add_a_name": "您需要添加名称",
|
||||
"commission_per_transaction": "每笔交易的佣金",
|
||||
@@ -846,7 +850,7 @@
|
||||
"user_away_description": "您试图预约的人已将自己置于离开状态,因此不接受新预约。",
|
||||
"meet_people_with_the_same_tokens": "会见使用相同令牌的人",
|
||||
"only_book_people_and_allow": "仅和拥有相同令牌、分布式自治组织或非同质化代币的人的预约。",
|
||||
"account_created_with_identity_provider": "您的账户是使用身份提供商创建的。",
|
||||
"account_created_with_identity_provider": "您的账户是使用身份提供方创建的。",
|
||||
"account_managed_by_identity_provider": "您的账户由 {{provider}} 管理",
|
||||
"account_managed_by_identity_provider_description": "要更改您的电子邮件,密码,启用两步验证及更多功能,请访问您的 {{provider}} 账户设置。",
|
||||
"signin_with_google": "使用Google登录",
|
||||
@@ -1178,6 +1182,7 @@
|
||||
"event_time_variable": "活动时间",
|
||||
"timezone_variable": "时区",
|
||||
"location_variable": "位置",
|
||||
"additional_notes_variable": "附加备注",
|
||||
"organizer_name_variable": "组织者姓名",
|
||||
"app_upgrade_description": "要使用此功能,您需要升级到专业版帐户。",
|
||||
"invalid_number": "电话号码无效",
|
||||
@@ -1465,8 +1470,8 @@
|
||||
"error_editing_availability": "编辑可预约时间时出错",
|
||||
"dont_have_permission": "您无权访问此资源。",
|
||||
"saml_config": "SAML",
|
||||
"saml_configuration_placeholder": "请将从身份提供商获取的 SAML metadata 粘贴到这里",
|
||||
"saml_email_required": "请输入您的邮箱,以便我们找到您的 SAML 身份提供商",
|
||||
"saml_configuration_placeholder": "请将从身份提供方获取的 SAML metadata 粘贴到这里",
|
||||
"saml_email_required": "请输入您的邮箱,以便我们找到您的 SAML 身份提供方",
|
||||
"saml_sp_title": "服务提供商详细信息",
|
||||
"saml_sp_description": "您的身份提供程序 (IdP) 将要求您提供以下详细信息以完成 SAML 应用程序配置。",
|
||||
"saml_sp_acs_url": "ACS 链接",
|
||||
@@ -1939,5 +1944,5 @@
|
||||
"insights_user_filter": "用户:{{userName}}",
|
||||
"insights_subtitle": "查看您的活动的预约洞察",
|
||||
"custom_plan": "自定义计划",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在上面这里添加新的字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在此上方添加您的新字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"identity_provider": "身分提供者",
|
||||
"trial_days_left": "試用 PRO 方案還有 $t(day, {\"count\": {{days}} })",
|
||||
"trial_days_left": "您的 PRO 試用期還有 $t(day, {\"count\": {{days}} }) 到期",
|
||||
"day_one": "{{count}} 天",
|
||||
"day_other": "{{count}} 天",
|
||||
"second_one": "{{count}} 秒",
|
||||
@@ -349,7 +349,7 @@
|
||||
"no_meeting_found": "找不到會議",
|
||||
"no_meeting_found_description": "會議不存在。請聯繫會議主持人取得新連結。",
|
||||
"no_status_bookings_yet": "暫無 {{status}} 的預約",
|
||||
"no_status_bookings_yet_description": "沒有{{status}}的預約。{{description}}",
|
||||
"no_status_bookings_yet_description": "沒有 {{status}} 的預約。{{description}}",
|
||||
"event_between_users": "{{host}} 和 {{attendeeName}} 的 {{eventName}}",
|
||||
"bookings": "預約",
|
||||
"booking_not_found": "找不到預約",
|
||||
@@ -398,7 +398,7 @@
|
||||
"browse_api_documentation": "瀏覽 API 文件",
|
||||
"leverage_our_api": "請充分利用 API 徹底掌握與客製化。",
|
||||
"create_webhook": "建立 Webhook",
|
||||
"booking_cancelled": "預約已取消",
|
||||
"booking_cancelled": "已取消預約",
|
||||
"booking_rescheduled": "已重新安排預約",
|
||||
"recording_ready": "錄製內容下載連結已就緒",
|
||||
"booking_created": "已建立預約",
|
||||
@@ -411,17 +411,17 @@
|
||||
"create_new_webhook": "建立新的 Webhook",
|
||||
"webhooks": "Webhook",
|
||||
"team_webhooks": "團隊 Webhook",
|
||||
"create_new_webhook_to_account": "建立這個帳號的新 Webhook",
|
||||
"create_new_webhook_to_account": "為您的帳號建立一個的新 Webhook",
|
||||
"new_webhook": "新 Webhook",
|
||||
"receive_cal_meeting_data": "預定或取消活動時,即時在指定的網址收到 {{appName}} 會議資料。",
|
||||
"receive_cal_event_meeting_data": "預定或取消活動時,即時在指定的網址收到 {{appName}} 會議資料。",
|
||||
"responsive_fullscreen_iframe": "全螢幕響應式 Iframe",
|
||||
"responsive_fullscreen_iframe": "全螢幕響應式 iframe",
|
||||
"loading": "讀取中...",
|
||||
"deleting": "正在刪除......",
|
||||
"standard_iframe": "標準 Iframe",
|
||||
"standard_iframe": "標準 iframe",
|
||||
"developer": "開發人員",
|
||||
"manage_developer_settings": "管理您的開發人員設定。",
|
||||
"iframe_embed": "嵌入式 Iframe",
|
||||
"iframe_embed": "嵌入式 iframe",
|
||||
"embed_calcom": "在網站上嵌入 {{appName}} 最簡單的方法。",
|
||||
"integrate_using_embed_or_webhooks": "使用嵌入的方法整合進網站,或是以自訂 Webhook 的方式獲取即時預約資訊。",
|
||||
"schedule_a_meeting": "預定會議",
|
||||
@@ -846,7 +846,7 @@
|
||||
"user_away_description": "試圖預約的人已經設定為離開狀態,因此不接受新預約。",
|
||||
"meet_people_with_the_same_tokens": "認識有相同 Token 的人",
|
||||
"only_book_people_and_allow": "只預約/接受預約有相同 Token、DAO 或 NFT 的人。",
|
||||
"account_created_with_identity_provider": "帳號是使用 Identity Provider 建立的。",
|
||||
"account_created_with_identity_provider": "帳號是使用身分提供者建立的。",
|
||||
"account_managed_by_identity_provider": "帳號歸 {{provider}} 管轄",
|
||||
"account_managed_by_identity_provider_description": "要更改電子郵件、密碼、兩階段認證⋯⋯等,請造訪 {{provider}} 的帳號設定。",
|
||||
"signin_with_google": "透過 Google 登入",
|
||||
@@ -1178,6 +1178,7 @@
|
||||
"event_time_variable": "活動時間",
|
||||
"timezone_variable": "時區",
|
||||
"location_variable": "地點",
|
||||
"additional_notes_variable": "備註",
|
||||
"organizer_name_variable": "主辦者姓名",
|
||||
"app_upgrade_description": "您必須升級至專業版帳號才能使用此功能。",
|
||||
"invalid_number": "電話號碼無效",
|
||||
@@ -1465,8 +1466,8 @@
|
||||
"error_editing_availability": "編輯開放時間時發生錯誤",
|
||||
"dont_have_permission": "您沒有權限存取此資源。",
|
||||
"saml_config": "SAML",
|
||||
"saml_configuration_placeholder": "請把 Identity Provider 的 SAML Metadata 貼到這裡。",
|
||||
"saml_email_required": "請輸入讓我們可以找到 SAML Identity Provider 的電子郵件",
|
||||
"saml_configuration_placeholder": "請把身分提供者的 SAML Metadata 貼到這裡。",
|
||||
"saml_email_required": "請輸入讓我們可以找到 SAML 身分提供者的電子郵件",
|
||||
"saml_sp_title": "服務供應方詳細資訊",
|
||||
"saml_sp_description": "您的 Identity Provider (IdP) 會要求您提供下列詳細資料,以完成 SAML 應用程式設定。",
|
||||
"saml_sp_acs_url": "ACS 網址",
|
||||
@@ -1939,5 +1940,5 @@
|
||||
"insights_user_filter": "使用者:{{userName}}",
|
||||
"insights_subtitle": "查看所有活動的預約洞見",
|
||||
"custom_plan": "自訂方案",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在上方新增您的新字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在此上方添加您的新字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ it("can find lucky user with maximize availability", async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
prismaMock.user.findMany.mockResolvedValue(users);
|
||||
prismaMock.booking.findMany.mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
getLuckyUser("MAXIMIZE_AVAILABILITY", {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* # Regex for translation keys
|
||||
* This regex matches the following function calls:
|
||||
* 1. `t("<some_key>")`,
|
||||
* 2. `t("<some_key>", {key: "<some_string>"})`, or
|
||||
* 3. `<Trans i18nKey="home_hero_subtitle" t={t}>`
|
||||
*
|
||||
* It also ensure that we don't match any other similar function calls (e.g. `format("dddd")`).
|
||||
*
|
||||
* ## Explanation of the regex
|
||||
* - (?<!\w): negative lookbehind to ensure that there is no word character before "t" or "i18nKey"
|
||||
* - (?: ... ): non-capturing group
|
||||
* - t: for the use of `t("..")` OR `t("..", {key: ".."})`
|
||||
* - \(("[^"]*"): captures a string enclosed in double quotes followed by an opening parenthesis
|
||||
* - (?:,\s*\{[^}]*\})?: optional non-capturing group so that we match the optional interpolation object
|
||||
* - i18nKey=".+"[^\w]: for the usage of `<Trans i18nKey="home_hero_subtitle" t={t}>`
|
||||
*
|
||||
* */
|
||||
const translationKeyRegex = /(?<!\w)(?:t\(("[^"]*")(?:,\s*\{[^}]*\})?\)|i18nKey=".+"[^\w])/gi;
|
||||
|
||||
/** @type {import("i18n-unused/src/types/index.ts").RunOptions} */
|
||||
const config = {
|
||||
// localesPath: localePath, // uncomment to run on all locales (to calculate kb savings)
|
||||
localesPath: path.join("./apps/website", "/public/static/locales", "/en"),
|
||||
srcPath: "./apps/website",
|
||||
srcExtensions: ["ts", "tsx"],
|
||||
translationContextSeparator: ":",
|
||||
translationKeyMatcher: translationKeyRegex,
|
||||
missedTranslationParser: translationKeyRegex,
|
||||
ignorePaths: ["node_modules", ".next"],
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
+3
-2
@@ -51,10 +51,9 @@
|
||||
"lint:fix": "turbo run lint:fix",
|
||||
"lint:report": "turbo run lint:report",
|
||||
"lint": "turbo run lint",
|
||||
"postinstall": "turbo run post-install",
|
||||
"postinstall": "husky install && turbo run post-install",
|
||||
"pre-commit": "lint-staged",
|
||||
"predev": "echo 'Checking env files'",
|
||||
"prepare": "husky install",
|
||||
"prisma": "yarn workspace @calcom/prisma prisma",
|
||||
"start": "turbo run start --scope=\"@calcom/web\"",
|
||||
"tdd": "vitest watch",
|
||||
@@ -77,10 +76,12 @@
|
||||
"@deploysentinel/playwright": "^0.3.3",
|
||||
"@playwright/test": "^1.31.2",
|
||||
"@snaplet/copycat": "^0.3.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@types/dompurify": "^2.4.0",
|
||||
"c8": "^7.13.0",
|
||||
"dotenv-checker": "^1.1.5",
|
||||
"husky": "^8.0.0",
|
||||
"i18n-unused": "^0.13.0",
|
||||
"jest-diff": "^29.5.0",
|
||||
"jsdom": "^22.0.0",
|
||||
"lint-staged": "^12.5.0",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
|
||||
export const APP_STORE_PATH = path.join(__dirname, "..", "..", "app-store");
|
||||
export const TEMPLATES_PATH = path.join(APP_STORE_PATH, "templates");
|
||||
export const IS_WINDOWS_PLATFORM = os.platform() === "win32";
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants";
|
||||
import { APP_STORE_PATH, TEMPLATES_PATH, IS_WINDOWS_PLATFORM} from "./constants";
|
||||
import execSync from "./utils/execSync";
|
||||
|
||||
const slugify = (str: string) => {
|
||||
@@ -69,8 +69,8 @@ export const BaseAppFork = {
|
||||
}) {
|
||||
const appDirPath = getAppDirPath(slug, isTemplate);
|
||||
if (!editMode) {
|
||||
await execSync(`mkdir -p ${appDirPath}`);
|
||||
await execSync(`cp -r ${TEMPLATES_PATH}/${template}/* ${appDirPath}`);
|
||||
await execSync(IS_WINDOWS_PLATFORM ? `mkdir ${appDirPath}` : `mkdir -p ${appDirPath}`);
|
||||
await execSync(IS_WINDOWS_PLATFORM ? `xcopy "${TEMPLATES_PATH}\\${template}\\*" "${appDirPath}" /e /i` : `cp -r ${TEMPLATES_PATH}/${template}/* ${appDirPath}`);
|
||||
} else {
|
||||
if (!oldSlug) {
|
||||
throw new Error("oldSlug is required when editMode is true");
|
||||
@@ -79,7 +79,7 @@ export const BaseAppFork = {
|
||||
// We need to rename only if they are different
|
||||
const oldAppDirPath = getAppDirPath(oldSlug, isTemplate);
|
||||
|
||||
await execSync(`mv ${oldAppDirPath} ${appDirPath}`);
|
||||
await execSync(IS_WINDOWS_PLATFORM ? `move ${oldAppDirPath} ${appDirPath}` : `mv ${oldAppDirPath} ${appDirPath}`);
|
||||
}
|
||||
}
|
||||
updatePackageJson({ slug, appDirPath, appDescription: description });
|
||||
@@ -126,7 +126,7 @@ export const BaseAppFork = {
|
||||
|
||||
delete: async function ({ slug, isTemplate }: { slug: string; isTemplate: boolean }) {
|
||||
const appDirPath = getAppDirPath(slug, isTemplate);
|
||||
await execSync(`rm -rf ${appDirPath}`);
|
||||
await execSync(IS_WINDOWS_PLATFORM ? `rd /s /q ${appDirPath}` : `rm -rf ${appDirPath}`);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -38,27 +38,38 @@ export default function AppCard({
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"border-subtle mb-4",
|
||||
app.isInstalled ? "mt-2" : "mt-6",
|
||||
"rounded-md border",
|
||||
"border-subtle",
|
||||
app?.isInstalled && "mb-4 rounded-md border",
|
||||
!app.enabled && "grayscale"
|
||||
)}>
|
||||
<div className={classNames(app.isInstalled ? "p-4 text-sm sm:p-4" : "px-5 py-4 text-sm sm:px-5")}>
|
||||
<div className="flex w-full flex-col gap-2 sm:flex-row sm:gap-0">
|
||||
{/* Don't know why but w-[42px] isn't working, started happening when I started using next/dynamic */}
|
||||
<Link href={"/apps/" + app.slug} className="mr-3 h-auto w-10 rounded-sm">
|
||||
<Link
|
||||
href={"/apps/" + app.slug}
|
||||
className={classNames(app?.isInstalled ? "mr-[11px]" : "mr-3", "h-auto w-10 rounded-sm")}>
|
||||
<img
|
||||
className={classNames(
|
||||
app?.logo.includes("-dark") && "dark:invert",
|
||||
`w-full ${app.isInstalled ? "min-w-[42px]" : "min-w-[32.47px]"}`
|
||||
app?.isInstalled ? "min-w-[42px]" : "min-w-[40px]",
|
||||
"w-full"
|
||||
)}
|
||||
src={app?.logo}
|
||||
alt={app?.name}
|
||||
/>
|
||||
</Link>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-emphasis text-base font-semibold leading-4">{app?.name}</span>
|
||||
<p className="text-default max-w-md truncate pt-2 text-sm font-normal ltr:pr-2 rtl:pl-2">
|
||||
<div className="flex flex-col pe-3">
|
||||
<div className="text-emphasis">
|
||||
<span className={classNames(app?.isInstalled && "text-base", "font-semibold leading-4")}>
|
||||
{app?.name}
|
||||
</span>
|
||||
{!app?.isInstalled && (
|
||||
<span className="bg-emphasis ml-1 rounded px-1 py-0.5 text-xs font-medium leading-3 tracking-[0.01em]">
|
||||
{app?.categories[0].charAt(0).toUpperCase() + app?.categories[0].slice(1)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p title={app?.description} className="text-default line-clamp-1 pt-1 text-sm font-normal">
|
||||
{description || app?.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -105,7 +116,9 @@ export default function AppCard({
|
||||
</div>
|
||||
<div ref={animationRef}>
|
||||
{app?.isInstalled && switchChecked && <hr className="border-subtle" />}
|
||||
{app?.isInstalled && switchChecked ? <div className="p-4 text-sm sm:px-4">{children}</div> : null}
|
||||
{app?.isInstalled && switchChecked ? (
|
||||
<div className="p-4 pt-5 text-sm [&_input]:mb-0 [&_input]:leading-4">{children}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function OmniInstallAppButton({
|
||||
className="[@media(max-width:260px)]:w-full [@media(max-width:260px)]:justify-center"
|
||||
StartIcon={Plus}
|
||||
{...props}>
|
||||
{t("install")}
|
||||
{t("add")}
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { decodeOAuthState } from "./decodeOAuthState";
|
||||
import { throwIfNotHaveAdminAccessToTeam } from "./throwIfNotHaveAdminAccessToTeam";
|
||||
|
||||
/**
|
||||
* This function is used to create app credentials for either a user or a team
|
||||
@@ -19,8 +21,12 @@ const createOAuthAppCredential = async (
|
||||
req: NextApiRequest
|
||||
) => {
|
||||
const userId = req.session?.user.id;
|
||||
if (!userId) {
|
||||
throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" });
|
||||
}
|
||||
// For OAuth flows, see if a teamId was passed through the state
|
||||
const state = decodeOAuthState(req);
|
||||
|
||||
if (state?.teamId) {
|
||||
// Check that the user belongs to the team
|
||||
const team = await prisma.team.findFirst({
|
||||
@@ -50,6 +56,8 @@ const createOAuthAppCredential = async (
|
||||
return;
|
||||
}
|
||||
|
||||
await throwIfNotHaveAdminAccessToTeam({ teamId: state?.teamId ?? null, userId });
|
||||
|
||||
await prisma.credential.create({
|
||||
data: {
|
||||
type: appData.type,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import getUserAdminTeams from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
export const throwIfNotHaveAdminAccessToTeam = async ({
|
||||
teamId,
|
||||
userId,
|
||||
}: {
|
||||
teamId: number | null;
|
||||
userId: number;
|
||||
}) => {
|
||||
if (!teamId) {
|
||||
return;
|
||||
}
|
||||
const teamsUserHasAdminAccessFor = await getUserAdminTeams({ userId });
|
||||
const hasAdminAccessToTeam = teamsUserHasAdminAccessFor.some((team) => team.id === teamId);
|
||||
|
||||
if (!hasAdminAccessToTeam) {
|
||||
throw new HttpError({ statusCode: 401, message: "You must be an admin of the team to do this" });
|
||||
}
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import { throwIfNotHaveAdminAccessToTeam } from "../../_utils/throwIfNotHaveAdminAccessToTeam";
|
||||
|
||||
/**
|
||||
* This is an example endpoint for an app, these will run under `/api/integrations/[...args]`
|
||||
@@ -13,10 +14,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
if (!req.session?.user?.id) {
|
||||
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||
}
|
||||
|
||||
const userId = req.session.user.id;
|
||||
const appType = "giphy_other";
|
||||
const credentialOwner = req.query.teamId
|
||||
? { teamId: Number(req.query.teamId) }
|
||||
: { userId: req.session.user.id };
|
||||
const teamId = Number(req.query.teamId);
|
||||
const credentialOwner = req.query.teamId ? { teamId } : { userId: req.session.user.id };
|
||||
|
||||
await throwIfNotHaveAdminAccessToTeam({ teamId: teamId ?? null, userId });
|
||||
|
||||
try {
|
||||
const alreadyInstalled = await prisma.credential.findFirst({
|
||||
where: {
|
||||
|
||||
@@ -47,7 +47,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
// set expiry date as offset from current time.
|
||||
hubspotToken.expiryDate = Math.round(Date.now() + hubspotToken.expiresIn * 1000);
|
||||
|
||||
createOAuthAppCredential({ appId: "hubspot", type: "hubspot_other_calendar" }, hubspotToken as any, req);
|
||||
await createOAuthAppCredential({ appId: "hubspot", type: "hubspot_other_calendar" }, hubspotToken as any, req);
|
||||
|
||||
const state = decodeOAuthState(req);
|
||||
res.redirect(
|
||||
|
||||
@@ -93,7 +93,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
await prisma.credential.deleteMany({ where: { id: { in: credentialIdsToDelete }, userId } });
|
||||
}
|
||||
|
||||
createOAuthAppCredential({ appId: "msteams", type: "office365_video" }, responseBody, req);
|
||||
await createOAuthAppCredential({ appId: "msteams", type: "office365_video" }, responseBody, req);
|
||||
|
||||
const state = decodeOAuthState(req);
|
||||
return res.redirect(
|
||||
|
||||
@@ -23,6 +23,27 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
}}
|
||||
switchChecked={enabled}
|
||||
teamId={eventType.team?.id || undefined}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<TextField
|
||||
name="Plausible URL"
|
||||
defaultValue="https://plausible.io/js/script.js"
|
||||
placeholder="https://plausible.io/js/script.js"
|
||||
value={plausibleUrl}
|
||||
disabled={disabled}
|
||||
onChange={(e) => {
|
||||
setAppData("PLAUSIBLE_URL", e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
name="Tracked Domain"
|
||||
placeholder="yourdomain.com"
|
||||
value={trackingId}
|
||||
onChange={(e) => {
|
||||
setAppData("trackingId", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<TextField
|
||||
name="Plausible URL"
|
||||
defaultValue="https://plausible.io/js/script.js"
|
||||
|
||||
@@ -46,7 +46,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
}}
|
||||
switchChecked={enabled}
|
||||
teamId={eventType.team?.id || undefined}>
|
||||
<div className="flex w-full flex-col gap-2 text-sm">
|
||||
<div className="flex w-full flex-col gap-5 text-sm">
|
||||
<div className="flex w-full">
|
||||
<TextField
|
||||
name="hello"
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Controller } from "react-hook-form";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useOrgBrandingValues } from "@calcom/features/ee/organizations/hooks";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -145,7 +145,7 @@ function NewFormDialog({ appUrl }: { appUrl: string }) {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter showDivider className="mt-12 flex flex-row-reverse gap-x-2">
|
||||
<DialogFooter showDivider className="mt-12">
|
||||
<DialogClose />
|
||||
<Button loading={mutation.isLoading} data-testid="add-form" type="submit">
|
||||
{t("continue")}
|
||||
@@ -402,7 +402,7 @@ export const FormAction = forwardRef(function FormAction<T extends typeof Button
|
||||
const dropdownCtxValue = useContext(dropdownCtx);
|
||||
const dropdown = dropdownCtxValue?.dropdown;
|
||||
const embedLink = `forms/${routingForm?.id}`;
|
||||
const orgBranding = useOrgBrandingValues();
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
const formLink = `${orgBranding?.fullDomain ?? CAL_URL}/${embedLink}`;
|
||||
let redirectUrl = `${orgBranding?.fullDomain ?? CAL_URL}/router?form=${routingForm?.id}`;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||
import useApp from "@calcom/lib/hooks/useApp";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -540,7 +541,11 @@ export default function SingleFormWrapper({ form: _form, ...props }: SingleFormC
|
||||
if (!form) {
|
||||
throw new Error(t("something_went_wrong"));
|
||||
}
|
||||
return <SingleForm form={form} {...props} />;
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<SingleForm form={form} {...props} />
|
||||
</LicenseRequired>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSidePropsForSingleFormView = async function getServerSidePropsForSingleFormView(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import SkeletonLoaderTeamList from "@calcom/features/ee/teams/components/SkeletonloaderTeamList";
|
||||
import { FilterResults } from "@calcom/features/filters/components/FilterResults";
|
||||
import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
|
||||
@@ -119,182 +120,185 @@ export default function RoutingForms({
|
||||
];
|
||||
|
||||
return (
|
||||
<ShellMain
|
||||
heading="Routing Forms"
|
||||
CTA={hasPaidPlan && forms?.length ? <NewFormButton /> : null}
|
||||
subtitle={t("routing_forms_description")}>
|
||||
<UpgradeTip
|
||||
title={t("teams_plan_required")}
|
||||
description={t("routing_forms_are_a_great_way")}
|
||||
features={features}
|
||||
background="/tips/routing-forms"
|
||||
isParentLoading={<SkeletonLoaderTeamList />}
|
||||
buttons={
|
||||
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
||||
<ButtonGroup>
|
||||
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
|
||||
{t("upgrade")}
|
||||
</Button>
|
||||
<Button color="minimal" href="https://go.cal.com/teams-video" target="_blank">
|
||||
{t("learn_more")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
}>
|
||||
<FormActionsProvider appUrl={appUrl}>
|
||||
<div className="-mx-4 md:-mx-8">
|
||||
<div className="mb-10 w-full px-4 pb-2 sm:px-6 md:px-8">
|
||||
<div className="flex">
|
||||
<TeamsFilter />
|
||||
</div>
|
||||
<FilterResults
|
||||
queryRes={queryRes}
|
||||
emptyScreen={
|
||||
<EmptyScreen
|
||||
Icon={GitMerge}
|
||||
headline={t("create_your_first_form")}
|
||||
description={t("create_your_first_form_description")}
|
||||
buttonRaw={<NewFormButton />}
|
||||
/>
|
||||
}
|
||||
noResultsScreen={
|
||||
<EmptyScreen
|
||||
Icon={GitMerge}
|
||||
headline={t("no_results_for_filter")}
|
||||
description={t("change_filter_common")}
|
||||
/>
|
||||
}
|
||||
SkeletonLoader={SkeletonLoaderTeamList}>
|
||||
<div className="bg-default mb-16 overflow-hidden">
|
||||
<List data-testid="routing-forms-list">
|
||||
{forms?.map(({ form, readOnly }) => {
|
||||
if (!form) {
|
||||
return null;
|
||||
}
|
||||
<LicenseRequired>
|
||||
<ShellMain
|
||||
heading="Routing Forms"
|
||||
CTA={hasPaidPlan && forms?.length ? <NewFormButton /> : null}
|
||||
subtitle={t("routing_forms_description")}>
|
||||
<UpgradeTip
|
||||
title={t("teams_plan_required")}
|
||||
description={t("routing_forms_are_a_great_way")}
|
||||
features={features}
|
||||
background="/tips/routing-forms"
|
||||
isParentLoading={<SkeletonLoaderTeamList />}
|
||||
buttons={
|
||||
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
||||
<ButtonGroup>
|
||||
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
|
||||
{t("upgrade")}
|
||||
</Button>
|
||||
<Button color="minimal" href="https://go.cal.com/teams-video" target="_blank">
|
||||
{t("learn_more")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
}>
|
||||
<FormActionsProvider appUrl={appUrl}>
|
||||
<div className="-mx-4 md:-mx-8">
|
||||
<div className="mb-10 w-full px-4 pb-2 sm:px-6 md:px-8">
|
||||
<div className="flex">
|
||||
<TeamsFilter />
|
||||
</div>
|
||||
<FilterResults
|
||||
queryRes={queryRes}
|
||||
emptyScreen={
|
||||
<EmptyScreen
|
||||
Icon={GitMerge}
|
||||
headline={t("create_your_first_form")}
|
||||
description={t("create_your_first_form_description")}
|
||||
buttonRaw={<NewFormButton />}
|
||||
/>
|
||||
}
|
||||
noResultsScreen={
|
||||
<EmptyScreen
|
||||
Icon={GitMerge}
|
||||
headline={t("no_results_for_filter")}
|
||||
description={t("change_filter_common")}
|
||||
/>
|
||||
}
|
||||
SkeletonLoader={SkeletonLoaderTeamList}>
|
||||
<div className="bg-default mb-16 overflow-hidden">
|
||||
<List data-testid="routing-forms-list">
|
||||
{forms?.map(({ form, readOnly }) => {
|
||||
if (!form) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const description = form.description || "";
|
||||
form.routes = form.routes || [];
|
||||
const fields = form.fields || [];
|
||||
const userRoutes = form.routes.filter((route) => !isFallbackRoute(route));
|
||||
return (
|
||||
<ListLinkItem
|
||||
key={form.id}
|
||||
href={appUrl + "/form-edit/" + form.id}
|
||||
heading={form.name}
|
||||
disabled={readOnly}
|
||||
subHeading={description}
|
||||
className="space-x-2 rtl:space-x-reverse"
|
||||
actions={
|
||||
<>
|
||||
{form.team?.name && (
|
||||
<div className="border-r-2 border-neutral-300">
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
|
||||
{form.team.name}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<FormAction
|
||||
disabled={readOnly}
|
||||
className="self-center"
|
||||
action="toggle"
|
||||
routingForm={form}
|
||||
/>
|
||||
<ButtonGroup combined>
|
||||
<Tooltip content={t("preview")}>
|
||||
const description = form.description || "";
|
||||
form.routes = form.routes || [];
|
||||
const fields = form.fields || [];
|
||||
const userRoutes = form.routes.filter((route) => !isFallbackRoute(route));
|
||||
return (
|
||||
<ListLinkItem
|
||||
key={form.id}
|
||||
href={appUrl + "/form-edit/" + form.id}
|
||||
heading={form.name}
|
||||
disabled={readOnly}
|
||||
subHeading={description}
|
||||
className="space-x-2 rtl:space-x-reverse"
|
||||
actions={
|
||||
<>
|
||||
{form.team?.name && (
|
||||
<div className="border-r-2 border-neutral-300">
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
|
||||
{form.team.name}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<FormAction
|
||||
disabled={readOnly}
|
||||
className="self-center"
|
||||
action="toggle"
|
||||
routingForm={form}
|
||||
/>
|
||||
<ButtonGroup combined>
|
||||
<Tooltip content={t("preview")}>
|
||||
<FormAction
|
||||
action="preview"
|
||||
routingForm={form}
|
||||
target="_blank"
|
||||
StartIcon={ExternalLink}
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
/>
|
||||
</Tooltip>
|
||||
<FormAction
|
||||
action="preview"
|
||||
routingForm={form}
|
||||
target="_blank"
|
||||
StartIcon={ExternalLink}
|
||||
action="copyLink"
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon={LinkIcon}
|
||||
tooltip={t("copy_link_to_form")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<FormAction
|
||||
routingForm={form}
|
||||
action="copyLink"
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon={LinkIcon}
|
||||
tooltip={t("copy_link_to_form")}
|
||||
/>
|
||||
<FormAction
|
||||
routingForm={form}
|
||||
action="embed"
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon={Code}
|
||||
tooltip={t("embed")}
|
||||
/>
|
||||
<FormActionsDropdown disabled={readOnly}>
|
||||
<FormAction
|
||||
action="edit"
|
||||
routingForm={form}
|
||||
color="minimal"
|
||||
className="!flex"
|
||||
StartIcon={Edit}>
|
||||
{t("edit")}
|
||||
</FormAction>
|
||||
<FormAction
|
||||
action="download"
|
||||
routingForm={form}
|
||||
color="minimal"
|
||||
StartIcon={Download}>
|
||||
{t("download_responses")}
|
||||
</FormAction>
|
||||
<FormAction
|
||||
action="duplicate"
|
||||
routingForm={form}
|
||||
color="minimal"
|
||||
className="w-full"
|
||||
StartIcon={Copy}>
|
||||
{t("duplicate")}
|
||||
</FormAction>
|
||||
{typeformApp?.isInstalled ? (
|
||||
action="embed"
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon={Code}
|
||||
tooltip={t("embed")}
|
||||
/>
|
||||
<FormActionsDropdown disabled={readOnly}>
|
||||
<FormAction
|
||||
data-testid="copy-redirect-url"
|
||||
action="edit"
|
||||
routingForm={form}
|
||||
action="copyRedirectUrl"
|
||||
color="minimal"
|
||||
type="button"
|
||||
StartIcon={LinkIcon}>
|
||||
{t("Copy Typeform Redirect Url")}
|
||||
className="!flex"
|
||||
StartIcon={Edit}>
|
||||
{t("edit")}
|
||||
</FormAction>
|
||||
) : null}
|
||||
<FormAction
|
||||
action="_delete"
|
||||
routingForm={form}
|
||||
color="destructive"
|
||||
className="w-full"
|
||||
StartIcon={Trash}>
|
||||
{t("delete")}
|
||||
</FormAction>
|
||||
</FormActionsDropdown>
|
||||
</ButtonGroup>
|
||||
</>
|
||||
}>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Badge variant="gray" startIcon={Menu}>
|
||||
{fields.length} {fields.length === 1 ? "field" : "fields"}
|
||||
</Badge>
|
||||
<Badge variant="gray" startIcon={GitMerge}>
|
||||
{userRoutes.length} {userRoutes.length === 1 ? "route" : "routes"}
|
||||
</Badge>
|
||||
<Badge variant="gray" startIcon={MessageCircle}>
|
||||
{form._count.responses} {form._count.responses === 1 ? "response" : "responses"}
|
||||
</Badge>
|
||||
</div>
|
||||
</ListLinkItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</div>
|
||||
</FilterResults>
|
||||
<FormAction
|
||||
action="download"
|
||||
routingForm={form}
|
||||
color="minimal"
|
||||
StartIcon={Download}>
|
||||
{t("download_responses")}
|
||||
</FormAction>
|
||||
<FormAction
|
||||
action="duplicate"
|
||||
routingForm={form}
|
||||
color="minimal"
|
||||
className="w-full"
|
||||
StartIcon={Copy}>
|
||||
{t("duplicate")}
|
||||
</FormAction>
|
||||
{typeformApp?.isInstalled ? (
|
||||
<FormAction
|
||||
data-testid="copy-redirect-url"
|
||||
routingForm={form}
|
||||
action="copyRedirectUrl"
|
||||
color="minimal"
|
||||
type="button"
|
||||
StartIcon={LinkIcon}>
|
||||
{t("Copy Typeform Redirect Url")}
|
||||
</FormAction>
|
||||
) : null}
|
||||
<FormAction
|
||||
action="_delete"
|
||||
routingForm={form}
|
||||
color="destructive"
|
||||
className="w-full"
|
||||
StartIcon={Trash}>
|
||||
{t("delete")}
|
||||
</FormAction>
|
||||
</FormActionsDropdown>
|
||||
</ButtonGroup>
|
||||
</>
|
||||
}>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Badge variant="gray" startIcon={Menu}>
|
||||
{fields.length} {fields.length === 1 ? "field" : "fields"}
|
||||
</Badge>
|
||||
<Badge variant="gray" startIcon={GitMerge}>
|
||||
{userRoutes.length} {userRoutes.length === 1 ? "route" : "routes"}
|
||||
</Badge>
|
||||
<Badge variant="gray" startIcon={MessageCircle}>
|
||||
{form._count.responses}{" "}
|
||||
{form._count.responses === 1 ? "response" : "responses"}
|
||||
</Badge>
|
||||
</div>
|
||||
</ListLinkItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</div>
|
||||
</FilterResults>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormActionsProvider>
|
||||
</UpgradeTip>
|
||||
</ShellMain>
|
||||
</FormActionsProvider>
|
||||
</UpgradeTip>
|
||||
</ShellMain>
|
||||
</LicenseRequired>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Head from "next/head";
|
||||
import z from "zod";
|
||||
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import type { AppGetServerSidePropsContext, AppPrisma } from "@calcom/types/AppGetServerSideProps";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
|
||||
@@ -47,11 +48,21 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { form: formId, slug: _slug, pages: _pages, ...fieldsResponses } = queryParsed.data;
|
||||
const form = await prisma.app_RoutingForms_Form.findUnique({
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
||||
|
||||
const form = await prisma.app_RoutingForms_Form.findFirst({
|
||||
where: {
|
||||
id: formId,
|
||||
user: {
|
||||
organization: isValidOrgDomain
|
||||
? {
|
||||
slug: currentOrgDomain,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!form) {
|
||||
return {
|
||||
notFound: true,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Toaster } from "react-hot-toast";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import useGetBrandingColours from "@calcom/lib/getBrandColours";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -246,11 +247,20 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
||||
|
||||
const isEmbed = params.appPages[1] === "embed";
|
||||
|
||||
const form = await prisma.app_RoutingForms_Form.findUnique({
|
||||
const form = await prisma.app_RoutingForms_Form.findFirst({
|
||||
where: {
|
||||
id: formId,
|
||||
user: {
|
||||
organization: isValidOrgDomain
|
||||
? {
|
||||
slug: currentOrgDomain,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
|
||||
@@ -236,10 +236,7 @@ test.describe("Routing Forms", () => {
|
||||
await user.apiLogin();
|
||||
|
||||
await page.goto(`/apps/routing-forms/reporting/${routingForm.id}`);
|
||||
// Can't keep waiting forever. So, added a timeout of 5000ms
|
||||
await page.waitForResponse((response) => response.url().includes("appRoutingForms/report"), {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
const headerEls = page.locator("[data-testid='reporting-header'] th");
|
||||
// Once the response is there, React would soon render it, so 500ms is enough
|
||||
// FIXME: Sometimes it takes more than 500ms, so added a timeout of 1000ms for now. There might be something wrong with rendering.
|
||||
|
||||
@@ -38,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
const salesforceTokenInfo = await conn.oauth2.requestToken(code as string);
|
||||
|
||||
createOAuthAppCredential(
|
||||
await createOAuthAppCredential(
|
||||
{ appId: "salesforce", type: "salesforce_other_calendar" },
|
||||
salesforceTokenInfo as any,
|
||||
req
|
||||
|
||||
@@ -44,7 +44,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
data["default_currency"] = account.default_currency;
|
||||
}
|
||||
|
||||
createOAuthAppCredential(
|
||||
await createOAuthAppCredential(
|
||||
{ appId: "stripe", type: "stripe_payment" },
|
||||
data as unknown as Prisma.InputJsonObject,
|
||||
req
|
||||
|
||||
@@ -60,7 +60,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000);
|
||||
delete responseBody.expires_in;
|
||||
|
||||
createOAuthAppCredential({ appId: "tandem", type: "tandem_video" }, responseBody, req);
|
||||
await createOAuthAppCredential({ appId: "tandem", type: "tandem_video" }, responseBody, req);
|
||||
|
||||
res.redirect(getInstalledAppPath({ variant: "conferencing", slug: "tandem" }));
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
await prisma.credential.deleteMany({ where: { id: { in: credentialIdsToDelete }, userId } });
|
||||
}
|
||||
|
||||
createOAuthAppCredential({ appId: config.slug, type: config.type }, responseBody, req);
|
||||
await createOAuthAppCredential({ appId: config.slug, type: config.type }, responseBody, req);
|
||||
|
||||
res.redirect(getInstalledAppPath({ variant: config.variant, slug: config.slug }));
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
tokenInfo.data.expiryDate = Math.round(Date.now() + tokenInfo.data.expires_in);
|
||||
tokenInfo.data.accountServer = accountsServer;
|
||||
|
||||
createOAuthAppCredential({ appId: appConfig.slug, type: appConfig.type }, tokenInfo.data, req);
|
||||
await createOAuthAppCredential({ appId: appConfig.slug, type: appConfig.type }, tokenInfo.data, req);
|
||||
|
||||
const state = decodeOAuthState(req);
|
||||
res.redirect(
|
||||
|
||||
@@ -51,7 +51,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
zohoCrmTokenInfo.data.expiryDate = Math.round(Date.now() + 60 * 60);
|
||||
zohoCrmTokenInfo.data.accountServer = req.query["accounts-server"];
|
||||
|
||||
createOAuthAppCredential(
|
||||
await createOAuthAppCredential(
|
||||
{ appId: "zohocrm", type: "zohocrm_other_calendar" },
|
||||
zohoCrmTokenInfo.data as any,
|
||||
req
|
||||
|
||||
@@ -70,7 +70,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
await prisma.credential.deleteMany({ where: { id: { in: credentialIdsToDelete }, userId } });
|
||||
}
|
||||
|
||||
createOAuthAppCredential({ appId: "zoom", type: "zoom_video" }, responseBody, req);
|
||||
await createOAuthAppCredential({ appId: "zoom", type: "zoom_video" }, responseBody, req);
|
||||
|
||||
res.redirect(getInstalledAppPath({ variant: "conferencing", slug: "zoom" }));
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user