Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b83ee2d57d | ||
|
|
5f39bbe730 | ||
|
|
cf3d8cb4bb | ||
|
|
fbe0018d01 | ||
|
|
6586ed4d87 | ||
|
|
81e57afcf4 | ||
|
|
5cc530593b | ||
|
|
4111f78431 | ||
|
|
def80ea4cf | ||
|
|
2fc4676f6d | ||
|
|
318bafaab8 | ||
|
|
fc10c29037 | ||
|
|
ccb7b1f9ac | ||
|
|
c6c6544f50 | ||
|
|
8a6b93a747 | ||
|
|
ff6c6ccec0 | ||
|
|
c8db177f7d | ||
|
|
8871e1eb21 | ||
|
|
92b2d57871 | ||
|
|
77623c72c5 | ||
|
|
3239976e18 | ||
|
|
611939ffe8 | ||
|
|
1b8ecbe449 | ||
|
|
cd4882429d | ||
|
|
4fd2d80637 | ||
|
|
a343d934fe | ||
|
|
933cc739bd | ||
|
|
c7c75ff4d3 | ||
|
|
41f1920a7a | ||
|
|
e4836ea8f3 | ||
|
|
bfa30aa504 | ||
|
|
b36043b637 | ||
|
|
75026289fd | ||
|
|
80649c9f1a | ||
|
|
7dd75afb63 | ||
|
|
f66e4aeeed | ||
|
|
8225e61710 | ||
|
|
175fc4fcf3 | ||
|
|
f20a5df322 | ||
|
|
62544931b2 | ||
|
|
f6ad1d6ed5 | ||
|
|
d22b1072b5 | ||
|
|
4b356dd937 | ||
|
|
1f51199a7b | ||
|
|
b80a631998 | ||
|
|
bef4422080 | ||
|
|
cfd1a72c64 | ||
|
|
23b3a6661c | ||
|
|
b8b6c48d7d | ||
|
|
81655f9988 | ||
|
|
ff74822c48 | ||
|
|
8ec178debd | ||
|
|
f881061d8d | ||
|
|
2525559d6b | ||
|
|
38fb7a3f77 | ||
|
|
08956e54c5 | ||
|
|
ea65a2a27e | ||
|
|
0d45cfb7ea | ||
|
|
a4fc25e260 | ||
|
|
796c932311 | ||
|
|
4137f63ec7 | ||
|
|
2147cc82a5 | ||
|
|
40e646e9a4 | ||
|
|
cb2225259c | ||
|
|
372cd94d9f | ||
|
|
c7c282f30f | ||
|
|
959488910f | ||
|
|
0ca0cc3265 | ||
|
|
69bc12187e | ||
|
|
5e772530b2 | ||
|
|
3088b1174a | ||
|
|
fadf12b2bd | ||
|
|
c81f30b092 | ||
|
|
476119d627 | ||
|
|
040f03a9b5 | ||
|
|
b552bf6956 | ||
|
|
82d3c4370d | ||
|
|
492b4d3e97 | ||
|
|
0ae319d206 | ||
|
|
749311be2a | ||
|
|
3cce4e5009 | ||
|
|
341f82ccb9 | ||
|
|
e028a339f5 | ||
|
|
c55e432bc3 | ||
|
|
f47c3cd538 | ||
|
|
4b70a20941 | ||
|
|
c873b83965 | ||
|
|
ff859737ec | ||
|
|
607ef71f9f | ||
|
|
24dd4002fa | ||
|
|
078c9ddca5 | ||
|
|
b44e5a7aae | ||
|
|
4374da08e2 | ||
|
|
98b531e6b5 | ||
|
|
74414db760 | ||
|
|
bdf3e34ea1 | ||
|
|
738e0f3e50 | ||
|
|
16ae19abdb | ||
|
|
a2e18ae9ba | ||
|
|
e2cfd0d1d1 | ||
|
|
067b77faf1 | ||
|
|
740c01ac29 | ||
|
|
97cfd1ffa7 | ||
|
|
a7b2cfc90f | ||
|
|
d067b0782a | ||
|
|
203a36e2b5 | ||
|
|
f5a38c3f89 |
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
name: TODO to Issue
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
MANUAL_COMMIT_REF:
|
||||
description: "The SHA of the commit to get the diff for"
|
||||
required: true
|
||||
MANUAL_BASE_REF:
|
||||
description: "By default, the commit entered above is compared to the one directly before it; to go back further, enter an earlier SHA here"
|
||||
required: false
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
todos:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: "Add TODOs to issues"
|
||||
uses: "alstr/todo-to-issue-action@v4"
|
||||
env:
|
||||
MANUAL_COMMIT_REF: ${{ inputs.MANUAL_COMMIT_REF }}
|
||||
MANUAL_BASE_REF: ${{ inputs.MANUAL_BASE_REF }}
|
||||
with:
|
||||
IDENTIFIERS: '[{"name": "TODO", "labels": ["🙋🏻♂️help wanted"]}, {"name": "FIXME", "labels": ["🐛 bug"]}]'
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
tasks:
|
||||
- init: |
|
||||
yarn &&
|
||||
cp .env.example .env &&
|
||||
next_auth_secret=$(openssl rand -base64 32) &&
|
||||
calendso_encryption_key=$(openssl rand -base64 24) &&
|
||||
sed -i -e "s|^NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=$next_auth_secret|" \
|
||||
-e "s|^CALENDSO_ENCRYPTION_KEY=.*|CALENDSO_ENCRYPTION_KEY=$calendso_encryption_key|" .env
|
||||
command: yarn dx
|
||||
|
||||
ports:
|
||||
- port: 3000
|
||||
visibility: public
|
||||
onOpen: open-preview
|
||||
- port: 5420
|
||||
visibility: private
|
||||
onOpen: ignore
|
||||
- port: 1025
|
||||
visibility: private
|
||||
onOpen: ignore
|
||||
- port: 8025
|
||||
visibility: private
|
||||
onOpen: ignore
|
||||
|
||||
github:
|
||||
prebuilds:
|
||||
master: true
|
||||
pullRequests: true
|
||||
pullRequestsFromForks: true
|
||||
addCheck: true
|
||||
addComment: true
|
||||
addBadge: true
|
||||
|
||||
vscode:
|
||||
extensions:
|
||||
- DavidAnson.vscode-markdownlint
|
||||
- yzhang.markdown-all-in-one
|
||||
- esbenp.prettier-vscode
|
||||
- dbaeumer.vscode-eslint
|
||||
- bradlc.vscode-tailwindcss
|
||||
- ban.spellright
|
||||
- stripe.vscode-stripe
|
||||
- Prisma.prisma
|
||||
File diff suppressed because one or more lines are too long
@@ -3,5 +3,7 @@ nodeLinker: node-modules
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||
spec: "@yarnpkg/plugin-workspace-tools"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.4.1.cjs
|
||||
|
||||
+6
-1
@@ -86,7 +86,12 @@ To develop locally:
|
||||
yarn
|
||||
```
|
||||
|
||||
5. Start developing and watch for code changes:
|
||||
5. Set up your `.env` file:
|
||||
|
||||
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
||||
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||
|
||||
6. Start developing and watch for code changes:
|
||||
|
||||
```sh
|
||||
yarn dev
|
||||
|
||||
@@ -2,7 +2,7 @@ Copyright (c) 2020-present Cal.com, Inc.
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee directory of this repository (Enterprise Edition) is licensed under the license defined in "ee/LICENSE".
|
||||
* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee directory of this repository (Commercial License) is licensed under the license defined in "ee/LICENSE".
|
||||
* All third party components incorporated into the Cal.com Software are licensed under the original license provided by the owner of the applicable component.
|
||||
* Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
||||
|
||||
|
||||
@@ -158,6 +158,15 @@ yarn dx
|
||||
```sh
|
||||
echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||
```
|
||||
#### Gitpod Setup
|
||||
|
||||
1. Click the button below to open this project in Gitpod.
|
||||
|
||||
2. This will open a fully configured workspace in your browser with all the necessary dependencies already installed.
|
||||
|
||||
[](https://gitpod.io/#https://github.com/calcom/cal.com)
|
||||
|
||||
|
||||
|
||||
#### Manual setup
|
||||
|
||||
@@ -166,34 +175,29 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||
```
|
||||
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>If you don't know how to configure the DATABASE_URL, then follow the steps here to create a quick DB using Heroku.</summary>
|
||||
<summary>If you don't know how to configure the DATABASE_URL, then follow the steps here to create a quick local DB</summary>
|
||||
|
||||
1. Create a free account with [Heroku](https://heroku.com).
|
||||
1. [Download](https://www.postgresql.org/download/) and install postgres in your local (if you don't have it already).
|
||||
|
||||
2. Create a new app.
|
||||
<img width="306" alt="Create an App" src="https://user-images.githubusercontent.com/16905768/115322780-b3d58c00-a17e-11eb-8a52-b758fb0ea942.png">
|
||||
2. Create your own local db by executing `createDB <DB name>`
|
||||
|
||||
3. In your new app, go to `Overview` and next to `Installed add-ons`, click `Configure Add-ons`. We need this to set up our database.
|
||||

|
||||
3. Now open your psql shell with the DB you created: `psql -h localhost -U postgres -d <DB name>`
|
||||
|
||||
4. Once you clicked on `Configure Add-ons`, click on `Find more add-ons` and search for `postgres`. One of the options will be `Heroku Postgres` - click on that option.
|
||||

|
||||
4. Inside the psql shell execute `\conninfo`. And you will get the following info.
|
||||

|
||||
|
||||
5. Once the pop-up appears, click `Submit Order Form` - plan name should be `Hobby Dev - Free`.
|
||||
<img width="512" alt="Submit Order Form" src="https://user-images.githubusercontent.com/16905768/115323265-b4baed80-a17f-11eb-99f0-d67f019aa6df.png">
|
||||
5. Now extract all the info and add it to your DATABASE_URL. The url would look something like this
|
||||
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`.
|
||||
|
||||
6. Once you completed the above steps, click on your newly created `Heroku Postgres` and go to its `Settings`.
|
||||

|
||||
|
||||
7. In `Settings`, copy your URI to your Cal.com `.env` file and replace the `postgresql://<user>:<pass>@<db-host>:<db-port>` with it.
|
||||

|
||||

|
||||
|
||||
8. To view your DB, once you add new data in Prisma, you can use [Heroku Data Explorer](https://heroku-data-explorer.herokuapp.com).
|
||||
</details>
|
||||
|
||||
If you don't want to create a local DB. Then you can also consider using services like railway.app or render.
|
||||
- [Setup postgres DB with railway.app](https://arctype.com/postgres/setup/railway-postgres)
|
||||
- [Setup postgres DB with render](https://render.com/docs/databases)
|
||||
|
||||
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
|
||||
|
||||
1. Set a 32 character random string in your `.env` file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
|
||||
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
||||
|
||||
@@ -460,6 +464,10 @@ following
|
||||
9. Click the "Save" button at the bottom footer.
|
||||
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
|
||||
|
||||
### Obtaining Webex Client ID and Secret
|
||||
|
||||
[See Webex Readme](./packages/app-store/webex/)
|
||||
|
||||
### Obtaining ZohoCRM Client ID and Secret
|
||||
|
||||
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@calcom/docs",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# @calcom/web
|
||||
|
||||
## 2.7.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @calcom/embed-snippet@1.0.7
|
||||
- @calcom/embed-react@1.0.12
|
||||
- @calcom/embed-core@1.1.5
|
||||
@@ -36,6 +36,7 @@ const Component = ({
|
||||
tos,
|
||||
privacy,
|
||||
isProOnly,
|
||||
teamsPlanRequired,
|
||||
descriptionItems,
|
||||
isTemplate,
|
||||
dependencies,
|
||||
@@ -152,6 +153,7 @@ const Component = ({
|
||||
type={type}
|
||||
isProOnly={isProOnly}
|
||||
disableInstall={disableInstall}
|
||||
teamsPlanRequired={teamsPlanRequired}
|
||||
render={({ useDefaultComponent, ...props }) => {
|
||||
if (useDefaultComponent) {
|
||||
props = {
|
||||
@@ -192,6 +194,7 @@ const Component = ({
|
||||
type={type}
|
||||
isProOnly={isProOnly}
|
||||
disableInstall={disableInstall}
|
||||
teamsPlanRequired={teamsPlanRequired}
|
||||
render={({ useDefaultComponent, ...props }) => {
|
||||
if (useDefaultComponent) {
|
||||
props = {
|
||||
@@ -241,7 +244,9 @@ const Component = ({
|
||||
</div>
|
||||
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
|
||||
<span className="text-default">
|
||||
{price === 0 ? (
|
||||
{teamsPlanRequired ? (
|
||||
t("teams_plan_required")
|
||||
) : price === 0 ? (
|
||||
t("free_to_use_apps")
|
||||
) : (
|
||||
<>
|
||||
@@ -358,6 +363,7 @@ export default function App(props: {
|
||||
privacy?: string;
|
||||
licenseRequired: AppType["licenseRequired"];
|
||||
isProOnly: AppType["isProOnly"];
|
||||
teamsPlanRequired: AppType["teamsPlanRequired"];
|
||||
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
|
||||
isTemplate?: boolean;
|
||||
disableInstall?: boolean;
|
||||
|
||||
@@ -18,9 +18,9 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
||||
const { t } = useLocale();
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
const isAdmin = session.data?.user.role === "ADMIN";
|
||||
|
||||
if (session.status === "loading") return <></>;
|
||||
|
||||
return (
|
||||
<Shell {...rest} actions={actions?.("block")} hideHeadingOnMobile>
|
||||
<div className="flex flex-col xl:flex-row">
|
||||
@@ -29,8 +29,8 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
||||
<EmptyScreen
|
||||
Icon={AlertCircle}
|
||||
headline={t("no_apps")}
|
||||
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
|
||||
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
|
||||
description={isAdmin ? "You can enable apps in the settings" : ""}
|
||||
buttonText={isAdmin ? t("apps_settings") : ""}
|
||||
buttonOnClick={() => router.push("/settings/admin/apps/calendar")}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -72,7 +72,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
slotUtcStartDate: slot.time,
|
||||
eventTypeId,
|
||||
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
|
||||
bookingAttendees: bookingAttendees || undefined,
|
||||
bookingUid: slot.bookingUid,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
TableActions,
|
||||
TextAreaField,
|
||||
} from "@calcom/ui";
|
||||
import { Check, Clock, MapPin, RefreshCcw, Send, Slash, X, CreditCard } from "@calcom/ui/components/icon";
|
||||
import { Check, Clock, MapPin, RefreshCcw, Send, Ban, X, CreditCard } from "@calcom/ui/components/icon";
|
||||
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
@@ -119,7 +119,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
setRejectionDialogIsOpen(true);
|
||||
},
|
||||
icon: Slash,
|
||||
icon: Ban,
|
||||
disabled: mutation.isLoading,
|
||||
},
|
||||
// For bookings with payment, only confirm if the booking is paid for
|
||||
|
||||
@@ -237,7 +237,7 @@ const BookingPage = ({
|
||||
eventTypeId: eventType.id,
|
||||
slotUtcStartDate: dayjs(queryDate).utc().format(),
|
||||
slotUtcEndDate: dayjs(queryDate).utc().add(parseInt(queryDuration), "minutes").format(),
|
||||
bookingAttendees: currentSlotBooking ? currentSlotBooking.attendees.length : undefined,
|
||||
bookingUid: currentSlotBooking?.uid,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -322,7 +322,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
disabled={seatsLocked.disabled}
|
||||
defaultValue={value || 2}
|
||||
min={1}
|
||||
className="w-24"
|
||||
addOnSuffix={<>{t("seats")}</>}
|
||||
onChange={(e) => {
|
||||
onChange(Math.abs(Number(e.target.value)));
|
||||
|
||||
@@ -119,7 +119,7 @@ const EventTypeScheduleDetails = ({
|
||||
{format(dayRange.startTime, timeFormat === 12)}
|
||||
</span>
|
||||
<span className="ms-4">-</span>
|
||||
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
||||
<div className="ml-6 sm:w-28">{format(dayRange.endTime, timeFormat === 12)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -108,7 +108,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
||||
});
|
||||
|
||||
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||
const { t } = useLocale();
|
||||
const { t, i18n } = useLocale();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
const PERIOD_TYPES = [
|
||||
@@ -149,12 +149,25 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
const bookingLimitsLocked = shouldLockDisableProps("bookingLimits");
|
||||
const durationLimitsLocked = shouldLockDisableProps("durationLimits");
|
||||
const periodTypeLocked = shouldLockDisableProps("periodType");
|
||||
const offsetStartLockedProps = shouldLockDisableProps("offsetStart");
|
||||
|
||||
const optionsPeriod = [
|
||||
{ value: 1, label: t("calendar_days") },
|
||||
{ value: 0, label: t("business_days") },
|
||||
];
|
||||
|
||||
// offsetStart toggle is client-side only, opened by default if offsetStart is set
|
||||
const offsetStartValue = useWatch({
|
||||
control: formMethods.control,
|
||||
name: "offsetStart",
|
||||
});
|
||||
const [offsetToggle, setOffsetToggle] = useState(() => offsetStartValue > 0);
|
||||
|
||||
// Preview how the offset will affect start times
|
||||
const offsetOriginalTime = new Date();
|
||||
offsetOriginalTime.setHours(9, 0, 0, 0);
|
||||
const offsetAdjustedTime = new Date(offsetOriginalTime.getTime() + offsetStartValue * 60 * 1000);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4 lg:space-y-8">
|
||||
@@ -432,6 +445,32 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
</SettingsToggle>
|
||||
)}
|
||||
/>
|
||||
<hr className="border-subtle" />
|
||||
<SettingsToggle
|
||||
title={t("offset_toggle")}
|
||||
description={t("offset_toggle_description")}
|
||||
{...offsetStartLockedProps}
|
||||
checked={offsetToggle}
|
||||
onCheckedChange={(active) => {
|
||||
setOffsetToggle(active);
|
||||
if (!active) {
|
||||
formMethods.setValue("offsetStart", 0);
|
||||
}
|
||||
}}>
|
||||
<TextField
|
||||
required
|
||||
type="number"
|
||||
{...offsetStartLockedProps}
|
||||
label={t("offset_start")}
|
||||
defaultValue={eventType.offsetStart}
|
||||
{...formMethods.register("offsetStart")}
|
||||
addOnSuffix={<>{t("minutes")}</>}
|
||||
hint={t("offset_start_description", {
|
||||
originalTime: offsetOriginalTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
|
||||
adjustedTime: offsetAdjustedTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
|
||||
})}
|
||||
/>
|
||||
</SettingsToggle>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -390,7 +390,12 @@ export const EventSetupTab = (
|
||||
addOnLeading={
|
||||
<>
|
||||
{CAL_URL?.replace(/^(https?:|)\/\//, "")}/
|
||||
{team ? "team/" + team.slug : eventType.users[0].username}/
|
||||
{!isManagedEventType
|
||||
? team
|
||||
? "team/" + team.slug
|
||||
: eventType.users[0].username
|
||||
: t("username_placeholder")}
|
||||
/
|
||||
</>
|
||||
}
|
||||
{...formMethods.register("slug", {
|
||||
|
||||
@@ -170,7 +170,7 @@ export const EventTeamWebhooksTab = ({
|
||||
</Dialog>
|
||||
{/* Edit webhook dialog */}
|
||||
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
|
||||
<DialogContent title={t("edit_webhook")}>
|
||||
<DialogContent enableOverflow title={t("edit_webhook")}>
|
||||
<WebhookForm
|
||||
webhook={webhookToEdit}
|
||||
apps={installedApps?.items.map((app) => app.slug)}
|
||||
|
||||
@@ -170,7 +170,7 @@ function EventTypeSingleLayout({
|
||||
|
||||
// Define tab navigation here
|
||||
const EventTypeTabs = useMemo(() => {
|
||||
let navigation = getNavigation({
|
||||
const navigation = getNavigation({
|
||||
t,
|
||||
eventType,
|
||||
enabledAppsNumber,
|
||||
@@ -210,7 +210,7 @@ function EventTypeSingleLayout({
|
||||
}
|
||||
if (isManagedEventType || isChildrenManagedEventType) {
|
||||
// Removing apps and workflows for manageg event types by admins v1
|
||||
navigation = navigation.slice(0, -2);
|
||||
navigation.splice(-2, 1);
|
||||
} else {
|
||||
navigation.push({
|
||||
name: "webhooks",
|
||||
|
||||
+7
-5
@@ -3,15 +3,16 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
interface ICalendarItem {
|
||||
interface IAppConnectionItem {
|
||||
title: string;
|
||||
description?: string;
|
||||
logo: string;
|
||||
type: App["type"];
|
||||
installed?: boolean;
|
||||
}
|
||||
|
||||
const CalendarItem = (props: ICalendarItem) => {
|
||||
const { title, logo, type } = props;
|
||||
const AppConnectionItem = (props: IAppConnectionItem) => {
|
||||
const { title, logo, type, installed } = props;
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div className="flex flex-row items-center justify-between p-5">
|
||||
@@ -25,13 +26,14 @@ const CalendarItem = (props: ICalendarItem) => {
|
||||
<Button
|
||||
{...buttonProps}
|
||||
color="secondary"
|
||||
disabled={installed}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
// Save cookie key to return url step
|
||||
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
|
||||
buttonProps && buttonProps.onClick && buttonProps?.onClick(event);
|
||||
}}>
|
||||
{t("connect")}
|
||||
{installed ? t("installed") : t("connect")}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
@@ -39,4 +41,4 @@ const CalendarItem = (props: ICalendarItem) => {
|
||||
);
|
||||
};
|
||||
|
||||
export { CalendarItem };
|
||||
export { AppConnectionItem };
|
||||
@@ -0,0 +1,17 @@
|
||||
import { SkeletonAvatar, SkeletonText, SkeletonButton } from "@calcom/ui";
|
||||
|
||||
export function StepConnectionLoader() {
|
||||
return (
|
||||
<ul className="bg-default divide-subtle border-subtle divide-y rounded-md border p-0 dark:bg-black">
|
||||
{Array.from({ length: 4 }).map((_item, index) => {
|
||||
return (
|
||||
<li className="flex w-full flex-row justify-center border-b-0 py-6" key={index}>
|
||||
<SkeletonAvatar className="mx-6 h-8 w-8 px-4" />
|
||||
<SkeletonText className="ml-1 mr-4 mt-3 h-5 w-full" />
|
||||
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { SkeletonAvatar, SkeletonText, SkeletonButton, List } from "@calcom/ui";
|
||||
import { List } from "@calcom/ui";
|
||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import { CalendarItem } from "../components/CalendarItem";
|
||||
import { AppConnectionItem } from "../components/AppConnectionItem";
|
||||
import { ConnectedCalendarItem } from "../components/ConnectedCalendarItem";
|
||||
import { CreateEventsOnCalendarSelect } from "../components/CreateEventsOnCalendarSelect";
|
||||
import { StepConnectionLoader } from "../components/StepConnectionLoader";
|
||||
|
||||
interface IConnectCalendarsProps {
|
||||
nextStep: () => void;
|
||||
@@ -33,7 +33,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
||||
firstCalendar.integration.title &&
|
||||
firstCalendar.integration.logo && (
|
||||
<>
|
||||
<List className="bg-default border-subtle rounded-md border p-0 dark:bg-black">
|
||||
<List className="bg-default border-subtle rounded-md border p-0 dark:bg-black ">
|
||||
<ConnectedCalendarItem
|
||||
key={firstCalendar.integration.title}
|
||||
name={firstCalendar.integration.title}
|
||||
@@ -60,7 +60,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
||||
queryIntegrations.data.items.map((item) => (
|
||||
<li key={item.title}>
|
||||
{item.title && item.logo && (
|
||||
<CalendarItem
|
||||
<AppConnectionItem
|
||||
type={item.type}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
@@ -72,19 +72,8 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
||||
</List>
|
||||
)}
|
||||
|
||||
{queryIntegrations.isLoading && (
|
||||
<ul className="bg-default divide-subtle border-subtle divide-y rounded-md border p-0 dark:bg-black">
|
||||
{[0, 0, 0, 0].map((_item, index) => {
|
||||
return (
|
||||
<li className="flex w-full flex-row justify-center border-b-0 py-6" key={index}>
|
||||
<SkeletonAvatar className="mx-6 h-8 w-8 px-4" />
|
||||
<SkeletonText className="ml-1 mr-4 mt-3 h-5 w-full" />
|
||||
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
{queryIntegrations.isLoading && <StepConnectionLoader />}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
data-testid="save-calendar-button"
|
||||
@@ -95,7 +84,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
||||
onClick={() => nextStep()}
|
||||
disabled={disabledNextButton}>
|
||||
{firstCalendar ? `${t("continue")}` : `${t("next_step_text")}`}
|
||||
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { List } from "@calcom/ui";
|
||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import { AppConnectionItem } from "../components/AppConnectionItem";
|
||||
import { StepConnectionLoader } from "../components/StepConnectionLoader";
|
||||
|
||||
interface ConnectedAppStepProps {
|
||||
nextStep: () => void;
|
||||
}
|
||||
|
||||
const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
||||
const { nextStep } = props;
|
||||
const { data: queryConnectedVideoApps, isLoading } = trpc.viewer.integrations.useQuery({
|
||||
variant: "conferencing",
|
||||
onlyInstalled: false,
|
||||
});
|
||||
const { t } = useLocale();
|
||||
|
||||
const hasAnyInstalledVideoApps = queryConnectedVideoApps?.items.some(
|
||||
(item) => item.credentialIds.length > 0
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isLoading && (
|
||||
<List className="bg-default border-subtle divide-subtle scroll-bar mx-1 max-h-[45vh] divide-y !overflow-y-scroll rounded-md border p-0 sm:mx-0">
|
||||
{queryConnectedVideoApps?.items &&
|
||||
queryConnectedVideoApps?.items.map((item) => {
|
||||
if (item.slug === "daily-video") return null; // we dont want to show daily here as it is installed by default
|
||||
return (
|
||||
<li key={item.name}>
|
||||
{item.name && item.logo && (
|
||||
<AppConnectionItem
|
||||
type={item.type}
|
||||
title={item.name}
|
||||
description={item.description}
|
||||
logo={item.logo}
|
||||
installed={item.credentialIds.length > 0}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
|
||||
{isLoading && <StepConnectionLoader />}
|
||||
<button
|
||||
type="button"
|
||||
data-testid="save-video-button"
|
||||
className={classNames(
|
||||
"text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm",
|
||||
!hasAnyInstalledVideoApps ? "cursor-not-allowed opacity-20" : ""
|
||||
)}
|
||||
disabled={!hasAnyInstalledVideoApps}
|
||||
onClick={() => nextStep()}>
|
||||
{t("next_step_text")}
|
||||
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { ConnectedVideoStep };
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
@@ -9,6 +8,7 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import { Button, Form } from "@calcom/ui";
|
||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
interface ISetupAvailabilityProps {
|
||||
nextStep: () => void;
|
||||
@@ -81,7 +81,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
|
||||
type="submit"
|
||||
className="mt-2 w-full justify-center p-2 text-sm sm:mt-8"
|
||||
disabled={availabilityForm.formState.isSubmitting}>
|
||||
{t("next_step_text")} <ArrowRightIcon className="ml-2 h-4 w-4 self-center" />
|
||||
{t("next_step_text")} <ArrowRight className="ml-2 h-4 w-4 self-center" />
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
||||
import { useRouter } from "next/router";
|
||||
import type { FormEvent } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
@@ -11,6 +10,7 @@ import turndown from "@calcom/lib/turndownService";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
||||
|
||||
@@ -159,7 +159,7 @@ const UserProfile = (props: IUserProfileProps) => {
|
||||
type="submit"
|
||||
className="text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm">
|
||||
{t("finish")}
|
||||
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -10,6 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, TimezoneSelect } from "@calcom/ui";
|
||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||
|
||||
@@ -116,7 +116,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
||||
className="mt-8 flex w-full flex-row justify-center"
|
||||
disabled={mutation.isLoading}>
|
||||
{t("next_step_text")}
|
||||
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -31,7 +31,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
||||
{!isBioEmpty ? (
|
||||
<>
|
||||
<div
|
||||
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { StarIcon as StarIconSolid } from "@heroicons/react/solid";
|
||||
import classNames from "classnames";
|
||||
import { debounce, noop } from "lodash";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -15,6 +14,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
|
||||
import { Star as StarSolid } from "@calcom/ui/components/icon";
|
||||
import { Check, Edit2, ExternalLink } from "@calcom/ui/components/icon";
|
||||
|
||||
export enum UsernameChangeStatusEnum {
|
||||
@@ -230,12 +230,16 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
<div className="absolute top-0 right-2 flex flex-row">
|
||||
<span
|
||||
className={classNames(
|
||||
"mx-2 py-1",
|
||||
isInputUsernamePremium ? "text-orange-400" : "",
|
||||
"mx-2 py-2",
|
||||
isInputUsernamePremium ? "text-transparent" : "",
|
||||
usernameIsAvailable ? "" : ""
|
||||
)}>
|
||||
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] h-4 w-4" /> : <></>}
|
||||
{!isInputUsernamePremium && usernameIsAvailable ? <Check className="mt-2 h-4 w-4" /> : <></>}
|
||||
{isInputUsernamePremium ? <StarSolid className="mt-[2px] h-4 w-4 fill-orange-400" /> : <></>}
|
||||
{!isInputUsernamePremium && usernameIsAvailable ? (
|
||||
<Check className="mt-[2px] h-4 w-4" />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+105
-44
@@ -26,7 +26,10 @@ const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children
|
||||
);
|
||||
|
||||
// Workaround for https://github.com/vercel/next.js/issues/8592
|
||||
export type AppProps = Omit<NextAppProps<WithNonceProps & Record<string, unknown>>, "Component"> & {
|
||||
export type AppProps = Omit<
|
||||
NextAppProps<WithNonceProps & { themeBasis?: string } & Record<string, unknown>>,
|
||||
"Component"
|
||||
> & {
|
||||
Component: NextAppProps["Component"] & {
|
||||
requiresLicense?: boolean;
|
||||
isThemeSupported?: boolean;
|
||||
@@ -72,58 +75,21 @@ const enum ThemeSupport {
|
||||
Booking = "userConfigured",
|
||||
}
|
||||
|
||||
const CalcomThemeProvider = (
|
||||
props: PropsWithChildren<
|
||||
WithNonceProps & {
|
||||
isBookingPage?: boolean | ((arg: { router: NextRouter }) => boolean);
|
||||
isThemeSupported?: boolean;
|
||||
}
|
||||
>
|
||||
) => {
|
||||
// We now support the inverse of how we handled it in the past. Setting this to false will disable theme.
|
||||
// undefined or true means we use system theme
|
||||
type CalcomThemeProps = PropsWithChildren<
|
||||
Pick<AppProps["pageProps"], "nonce" | "themeBasis"> &
|
||||
Pick<AppProps["Component"], "isBookingPage" | "isThemeSupported">
|
||||
>;
|
||||
const CalcomThemeProvider = (props: CalcomThemeProps) => {
|
||||
const router = useRouter();
|
||||
const isBookingPage = (() => {
|
||||
if (typeof props.isBookingPage === "function") {
|
||||
return props.isBookingPage({ router: router });
|
||||
}
|
||||
|
||||
return props.isBookingPage;
|
||||
})();
|
||||
|
||||
const themeSupport = isBookingPage
|
||||
? ThemeSupport.Booking
|
||||
: // if isThemeSupported is explicitly false, we don't use theme there
|
||||
props.isThemeSupported === false
|
||||
? ThemeSupport.None
|
||||
: ThemeSupport.App;
|
||||
|
||||
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
|
||||
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
|
||||
// One such example is our Embeds Demo and Testing page at http://localhost:3100
|
||||
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
|
||||
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
|
||||
const isEmbedMode = typeof embedNamespace === "string";
|
||||
|
||||
const storageKey = isEmbedMode
|
||||
? `embed-theme-${embedNamespace}`
|
||||
: themeSupport === ThemeSupport.App
|
||||
? "app-theme"
|
||||
: themeSupport === ThemeSupport.Booking
|
||||
? "booking-theme"
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
nonce={props.nonce}
|
||||
enableColorScheme={false}
|
||||
enableSystem={themeSupport !== ThemeSupport.None}
|
||||
forcedTheme={forcedTheme}
|
||||
storageKey={storageKey}
|
||||
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
|
||||
// This is how login to dashboard soft navigation changes theme from light to dark
|
||||
key={storageKey}
|
||||
attribute="class">
|
||||
<ThemeProvider {...getThemeProviderProps({ props, isEmbedMode, embedNamespace, router })}>
|
||||
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
|
||||
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
|
||||
{typeof window !== "undefined" && !isEmbedMode && (
|
||||
@@ -140,6 +106,100 @@ const CalcomThemeProvider = (
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* The most important job for this fn is to generate correct storageKey for theme persistenc.
|
||||
* `storageKey` is important because that key is listened for changes(using [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event) and any pages opened will change it's theme based on that(as part of next-themes implementation).
|
||||
* Choosing the right storageKey avoids theme flickering caused by another page using different theme
|
||||
* So, we handle all the cases here namely,
|
||||
* - Both Booking Pages, /free/30min and /pro/30min but configured with different themes but being operated together.
|
||||
* - Embeds using different namespace. They can be completely themed different on the same page.
|
||||
* - Embeds using the same namespace but showing different cal.com links with different themes
|
||||
* - Embeds using the same namespace and showing same cal.com links with different themes(Different theme is possible for same cal.com link in case of embed because of theme config available in embed)
|
||||
* - App has different theme then Booking Pages.
|
||||
*
|
||||
* All the above cases have one thing in common, which is the origin and thus localStorage is shared and thus `storageKey` is critical to avoid theme flickering.
|
||||
*
|
||||
* Some things to note:
|
||||
* - There is a side effect of so many factors in `storageKey` that many localStorage keys will be created if a user goes through all these scenarios(e.g like booking a lot of different users)
|
||||
* - Some might recommend disabling localStorage persistence but that doesn't give good UX as then we would default to light theme always for a few seconds before switching to dark theme(if that's the user's preference).
|
||||
* - We can't disable [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event handling as well because changing theme in one tab won't change the theme without refresh in other tabs. That's again a bad UX
|
||||
* - Theme flickering becomes infinitely ongoing in case of embeds because of the browser's delay in processing `storage` event within iframes. Consider two embeds simulatenously opened with pages A and B. Note the timeline and keep in mind that it happened
|
||||
* because 'setItem(A)' and 'Receives storageEvent(A)' allowed executing setItem(B) in b/w because of the delay.
|
||||
* - t1 -> setItem(A) & Fires storageEvent(A) - On Page A) - Current State(A)
|
||||
* - t2 -> setItem(B) & Fires storageEvent(B) - On Page B) - Current State(B)
|
||||
* - t3 -> Receives storageEvent(A) & thus setItem(A) & thus fires storageEvent(A) (On Page B) - Current State(A)
|
||||
* - t4 -> Receives storageEvent(B) & thus setItem(B) & thus fires storageEvent(B) (On Page A) - Current State(B)
|
||||
* - ... and so on ...
|
||||
*/
|
||||
function getThemeProviderProps({
|
||||
props,
|
||||
isEmbedMode,
|
||||
embedNamespace,
|
||||
router,
|
||||
}: {
|
||||
props: Omit<CalcomThemeProps, "children">;
|
||||
isEmbedMode: boolean;
|
||||
embedNamespace: string | null;
|
||||
router: NextRouter;
|
||||
}) {
|
||||
const isBookingPage = (() => {
|
||||
if (typeof props.isBookingPage === "function") {
|
||||
return props.isBookingPage({ router: router });
|
||||
}
|
||||
return props.isBookingPage;
|
||||
})();
|
||||
|
||||
const themeSupport = isBookingPage
|
||||
? ThemeSupport.Booking
|
||||
: // if isThemeSupported is explicitly false, we don't use theme there
|
||||
props.isThemeSupported === false
|
||||
? ThemeSupport.None
|
||||
: ThemeSupport.App;
|
||||
|
||||
const isBookingPageThemSupportRequired = themeSupport === ThemeSupport.Booking;
|
||||
const themeBasis = props.themeBasis;
|
||||
|
||||
if ((isBookingPageThemSupportRequired || isEmbedMode) && !themeBasis) {
|
||||
console.warn(
|
||||
"`themeBasis` is required for booking page theme support. Not providing it will cause theme flicker."
|
||||
);
|
||||
}
|
||||
|
||||
const appearanceIdSuffix = themeBasis ? ":" + themeBasis : "";
|
||||
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
|
||||
let embedExplicitlySetThemeSuffix = "";
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const embedTheme = window.getEmbedTheme();
|
||||
if (embedTheme) {
|
||||
embedExplicitlySetThemeSuffix = ":" + embedTheme;
|
||||
}
|
||||
}
|
||||
|
||||
const storageKey = isEmbedMode
|
||||
? // Same Namespace, Same Organizer but different themes would still work seamless and not cause theme flicker
|
||||
// Even though it's recommended to use different namespaces when you want to theme differently on the same page but if the embeds are on different pages, the problem can still arise
|
||||
`embed-theme-${embedNamespace}${appearanceIdSuffix}${embedExplicitlySetThemeSuffix}`
|
||||
: themeSupport === ThemeSupport.App
|
||||
? "app-theme"
|
||||
: isBookingPageThemSupportRequired
|
||||
? `booking-theme${appearanceIdSuffix}`
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
storageKey,
|
||||
forcedTheme,
|
||||
themeSupport,
|
||||
nonce: props.nonce,
|
||||
enableColorScheme: false,
|
||||
enableSystem: themeSupport !== ThemeSupport.None,
|
||||
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
|
||||
// This is how login to dashboard soft navigation changes theme from light to dark
|
||||
key: storageKey,
|
||||
attribute: "class",
|
||||
};
|
||||
}
|
||||
|
||||
function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
|
||||
const flags = useFlags();
|
||||
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
|
||||
@@ -157,6 +217,7 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
||||
<TooltipProvider>
|
||||
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
|
||||
<CalcomThemeProvider
|
||||
themeBasis={props.pageProps.themeBasis}
|
||||
nonce={props.pageProps.nonce}
|
||||
isThemeSupported={props.Component.isThemeSupported}
|
||||
isBookingPage={props.Component.isBookingPage}>
|
||||
|
||||
@@ -111,12 +111,6 @@ const nextConfig = {
|
||||
transform: "lucide-react/dist/esm/icons/{{ kebabCase member }}",
|
||||
preventFullImport: true,
|
||||
},
|
||||
"@heroicons/react/solid": {
|
||||
transform: "@heroicons/react/solid/esm/{{ member }}",
|
||||
},
|
||||
"@heroicons/react/outline": {
|
||||
transform: "@heroicons/react/outline/esm/{{ member }}",
|
||||
},
|
||||
"@calcom/features/insights/components": {
|
||||
transform: "@calcom/features/insights/components/{{member}}",
|
||||
skipDefaultConversion: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "2.8.13",
|
||||
"version": "2.9.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -41,7 +41,6 @@
|
||||
"@daily-co/daily-js": "^0.37.0",
|
||||
"@formkit/auto-animate": "^1.0.0-beta.5",
|
||||
"@glidejs/glide": "^3.5.2",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@hookform/error-message": "^2.0.0",
|
||||
"@hookform/resolvers": "^2.9.7",
|
||||
"@next-auth/prisma-adapter": "^1.0.4",
|
||||
@@ -115,12 +114,11 @@
|
||||
"react-select": "^5.7.0",
|
||||
"react-timezone-select": "^1.4.0",
|
||||
"react-use-intercom": "1.5.1",
|
||||
"remark": "^14.0.2",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"rrule": "^2.7.1",
|
||||
"sanitize-html": "^2.10.0",
|
||||
"schema-dts": "^1.1.0",
|
||||
"short-uuid": "^4.2.0",
|
||||
"strip-markdown": "^5.0.0",
|
||||
"stripe": "^9.16.0",
|
||||
"superjson": "1.9.1",
|
||||
"tailwindcss-radix": "^2.6.0",
|
||||
@@ -153,6 +151,7 @@
|
||||
"@types/qrcode": "^1.4.3",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-phone-number-input": "^3.0.14",
|
||||
"@types/remove-markdown": "^0.3.1",
|
||||
"@types/sanitize-html": "^2.9.0",
|
||||
"@types/stripe": "^8.0.417",
|
||||
"@types/uuid": "8.3.1",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { BadgeCheckIcon } from "@heroicons/react/solid";
|
||||
import classNames from "classnames";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
@@ -23,11 +22,12 @@ import defaultEvents, {
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
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 { ArrowRight } from "@calcom/ui/components/icon";
|
||||
import { Verified, ArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||
@@ -37,7 +37,16 @@ import PageWrapper from "@components/PageWrapper";
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
|
||||
const { users, profile, eventTypes, isDynamicGroup, dynamicNames, dynamicUsernames, isSingleUser } = props;
|
||||
const {
|
||||
users,
|
||||
profile,
|
||||
eventTypes,
|
||||
isDynamicGroup,
|
||||
dynamicNames,
|
||||
dynamicUsernames,
|
||||
isSingleUser,
|
||||
markdownStrippedBio,
|
||||
} = props;
|
||||
const [user] = users; //To be used when we only have a single user, not dynamic group
|
||||
useTheme(user.theme);
|
||||
const { t } = useLocale();
|
||||
@@ -107,11 +116,9 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
<>
|
||||
<HeadSeo
|
||||
title={isDynamicGroup ? dynamicNames.join(", ") : nameOrUsername}
|
||||
description={
|
||||
isDynamicGroup ? `Book events with ${dynamicUsernames.join(", ")}` : (user.bio as string) || ""
|
||||
}
|
||||
description={isDynamicGroup ? `Book events with ${dynamicUsernames.join(", ")}` : markdownStrippedBio}
|
||||
meeting={{
|
||||
title: isDynamicGroup ? "" : `${user.bio}`,
|
||||
title: isDynamicGroup ? "" : markdownStrippedBio,
|
||||
profile: { name: `${profile.name}`, image: null },
|
||||
users: isDynamicGroup
|
||||
? dynamicUsernames.map((username, index) => ({ username, name: dynamicNames[index] }))
|
||||
@@ -131,12 +138,14 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
<Avatar imageSrc={user.avatar} size="xl" alt={nameOrUsername} />
|
||||
<h1 className="font-cal text-emphasis mb-1 text-3xl">
|
||||
{nameOrUsername}
|
||||
{user.verified && <BadgeCheckIcon className=" mx-1 -mt-1 inline h-6 w-6 text-blue-500" />}
|
||||
{user.verified && (
|
||||
<Verified className=" mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black" />
|
||||
)}
|
||||
</h1>
|
||||
{!isBioEmpty && (
|
||||
<>
|
||||
<div
|
||||
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: props.safeBio }}
|
||||
/>
|
||||
</>
|
||||
@@ -342,11 +351,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
const safeBio = markdownToSafeHTML(user.bio) || "";
|
||||
|
||||
const markdownStrippedBio = stripMarkdown(user?.bio || "");
|
||||
return {
|
||||
props: {
|
||||
users,
|
||||
safeBio,
|
||||
profile,
|
||||
// Dynamic group has no theme preference right now. It uses system theme.
|
||||
themeBasis: isDynamicGroup ? null : user.username,
|
||||
user: {
|
||||
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
||||
},
|
||||
@@ -361,6 +373,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
dynamicNames,
|
||||
dynamicUsernames,
|
||||
isSingleUser,
|
||||
markdownStrippedBio,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -177,6 +177,8 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
||||
slug: `${user.username}/${eventType.slug}`,
|
||||
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
||||
},
|
||||
// Dynamic group has no theme preference right now. It uses system theme.
|
||||
themeBasis: user.username,
|
||||
away: user?.away,
|
||||
isDynamic: false,
|
||||
trpcState: ssg.dehydrate(),
|
||||
@@ -307,6 +309,8 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
||||
props: {
|
||||
eventType: eventTypeObject,
|
||||
profile,
|
||||
// Dynamic group has no theme preference right now. It uses system theme.
|
||||
themeBasis: null,
|
||||
isDynamic: true,
|
||||
away: false,
|
||||
trpcState: ssg.dehydrate(),
|
||||
|
||||
@@ -293,6 +293,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
props: {
|
||||
away: user.away,
|
||||
profile,
|
||||
// Dynamic group has no theme preference right now. It uses system theme.
|
||||
themeBasis: isDynamicGroupBooking ? null : user.username,
|
||||
eventType: eventTypeObject,
|
||||
booking,
|
||||
currentSlotBooking: currentSlotBooking,
|
||||
|
||||
@@ -46,7 +46,8 @@ class MyDocument extends Document<Props> {
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#000000" />
|
||||
<meta name="msapplication-TileColor" content="#ff0000" />
|
||||
<meta name="theme-color" content="var(--cal-bg)" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f9fafb" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1C1C" />
|
||||
</Head>
|
||||
|
||||
<body
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { WebhookTriggerEvents } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -5,6 +6,8 @@ import { DailyLocationType } from "@calcom/app-store/locations";
|
||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
|
||||
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
|
||||
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 { defaultHandler } from "@calcom/lib/server";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
@@ -20,6 +23,38 @@ const downloadLinkSchema = z.object({
|
||||
download_link: z.string(),
|
||||
});
|
||||
|
||||
const triggerWebhook = async ({
|
||||
evt,
|
||||
downloadLink,
|
||||
booking,
|
||||
}: {
|
||||
evt: CalendarEvent;
|
||||
downloadLink: string;
|
||||
booking: {
|
||||
userId: number | undefined;
|
||||
eventTypeId: number | null;
|
||||
};
|
||||
}) => {
|
||||
const eventTrigger: WebhookTriggerEvents = "RECORDING_READY";
|
||||
// Send Webhook call if hooked to BOOKING.RECORDING_READY
|
||||
const subscriberOptions = {
|
||||
userId: booking.userId ?? 0,
|
||||
eventTypeId: booking.eventTypeId ?? 0,
|
||||
triggerEvent: eventTrigger,
|
||||
};
|
||||
const webhooks = await getWebhooks(subscriberOptions);
|
||||
|
||||
const promises = webhooks.map((webhook) =>
|
||||
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, {
|
||||
...evt,
|
||||
downloadLink,
|
||||
}).catch((e) => {
|
||||
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}`, e);
|
||||
})
|
||||
);
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
|
||||
return res.status(405).json({ message: "No SendGrid API key or email" });
|
||||
@@ -51,6 +86,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
uid: true,
|
||||
location: true,
|
||||
isRecorded: true,
|
||||
eventTypeId: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -105,31 +141,36 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
},
|
||||
});
|
||||
|
||||
const response = await getDownloadLinkOfCalVideoByRecordingId(recordingId);
|
||||
const downloadLinkResponse = downloadLinkSchema.parse(response);
|
||||
const downloadLink = downloadLinkResponse.download_link;
|
||||
|
||||
const evt: CalendarEvent = {
|
||||
type: booking.title,
|
||||
title: booking.title,
|
||||
description: booking.description || undefined,
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
organizer: {
|
||||
email: booking.user?.email || "Email-less",
|
||||
name: booking.user?.name || "Nameless",
|
||||
timeZone: booking.user?.timeZone || "Europe/London",
|
||||
language: { translate: t, locale: booking?.user?.locale ?? "en" },
|
||||
},
|
||||
attendees: attendeesList,
|
||||
uid: booking.uid,
|
||||
};
|
||||
|
||||
await triggerWebhook({
|
||||
evt,
|
||||
downloadLink,
|
||||
booking: { userId: booking?.user?.id, eventTypeId: booking.eventTypeId },
|
||||
});
|
||||
|
||||
const isSendingEmailsAllowed = IS_SELF_HOSTED || session?.user?.belongsToActiveTeam;
|
||||
|
||||
// send emails to all attendees only when user has team plan
|
||||
if (isSendingEmailsAllowed) {
|
||||
const response = await getDownloadLinkOfCalVideoByRecordingId(recordingId);
|
||||
|
||||
const downloadLinkResponse = downloadLinkSchema.parse(response);
|
||||
const downloadLink = downloadLinkResponse.download_link;
|
||||
|
||||
const evt: CalendarEvent = {
|
||||
type: booking.title,
|
||||
title: booking.title,
|
||||
description: booking.description || undefined,
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
organizer: {
|
||||
email: booking.user?.email || "Email-less",
|
||||
name: booking.user?.name || "Nameless",
|
||||
timeZone: booking.user?.timeZone || "Europe/London",
|
||||
language: { translate: t, locale: booking?.user?.locale ?? "en" },
|
||||
},
|
||||
attendees: attendeesList,
|
||||
uid: booking.uid,
|
||||
};
|
||||
|
||||
await sendDailyVideoRecordingEmails(evt, downloadLink);
|
||||
return res.status(200).json({ message: "Success" });
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { SatoriOptions } from "satori";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Meeting, App, Generic } from "@calcom/lib/OgImages";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
|
||||
const calFont = fetch(new URL("../../../../public/fonts/cal.ttf", import.meta.url)).then((res) =>
|
||||
res.arrayBuffer()
|
||||
@@ -75,12 +74,10 @@ export default async function handler(req: NextApiRequest) {
|
||||
imageType,
|
||||
});
|
||||
|
||||
const title_ = md.render(title).replace(/(<([^>]+)>)/gi, "");
|
||||
|
||||
const img = new ImageResponse(
|
||||
(
|
||||
<Meeting
|
||||
title={title_}
|
||||
title={title}
|
||||
profile={{ name: meetingProfileName, image: meetingImage }}
|
||||
users={names.map((name, index) => ({ name, username: usernames[index] }))}
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "fs";
|
||||
import matter from "gray-matter";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import type { GetStaticPaths, GetStaticPropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import path from "path";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -33,7 +34,26 @@ const sourceSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||
function SingleAppPage(props: inferSSRProps<typeof getStaticProps>) {
|
||||
// If it's not production environment, it would be a better idea to inform that the App is disabled.
|
||||
if (props.isAppDisabled) {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
// TODO: Improve disabled App UI. This is just a placeholder.
|
||||
return (
|
||||
<div className="p-2">
|
||||
This App seems to be disabled. If you are an admin, you can enable this app from{" "}
|
||||
<Link href="/settings/admin/apps" className="cursor-pointer text-blue-500 underline">
|
||||
here
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Disabled App should give 404 any ways in production.
|
||||
return null;
|
||||
}
|
||||
|
||||
const { source, data } = props;
|
||||
return (
|
||||
<App
|
||||
name={data.name}
|
||||
@@ -52,6 +72,7 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||
website={data.url}
|
||||
email={data.email}
|
||||
licenseRequired={data.licenseRequired}
|
||||
teamsPlanRequired={data.teamsPlanRequired}
|
||||
isProOnly={data.isProOnly}
|
||||
descriptionItems={source.data?.items as string[] | undefined}
|
||||
isTemplate={data.isTemplate}
|
||||
@@ -80,29 +101,43 @@ export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
|
||||
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
||||
if (typeof ctx.params?.slug !== "string") return { notFound: true };
|
||||
|
||||
const app = await prisma.app.findUnique({
|
||||
const appMeta = await getAppWithMetadata({
|
||||
slug: ctx.params?.slug,
|
||||
});
|
||||
|
||||
const appFromDb = await prisma.app.findUnique({
|
||||
where: { slug: ctx.params.slug.toLowerCase() },
|
||||
});
|
||||
|
||||
if (!app) return { notFound: true };
|
||||
const isAppAvailableInFileSystem = appMeta;
|
||||
const isAppDisabled = isAppAvailableInFileSystem && (!appFromDb || !appFromDb.enabled);
|
||||
|
||||
const singleApp = await getAppWithMetadata(app);
|
||||
if (process.env.NODE_ENV !== "production" && isAppDisabled) {
|
||||
return {
|
||||
props: {
|
||||
isAppDisabled: true as const,
|
||||
data: {
|
||||
...appMeta,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!singleApp) return { notFound: true };
|
||||
if (!appFromDb || !appMeta || isAppDisabled) return { notFound: true };
|
||||
|
||||
const isTemplate = singleApp.isTemplate;
|
||||
const appDirname = path.join(isTemplate ? "templates" : "", app.dirName);
|
||||
const isTemplate = appMeta.isTemplate;
|
||||
const appDirname = path.join(isTemplate ? "templates" : "", appFromDb.dirName);
|
||||
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
|
||||
const postFilePath = path.join(README_PATH);
|
||||
let source = "";
|
||||
|
||||
try {
|
||||
source = fs.readFileSync(postFilePath).toString();
|
||||
source = source.replace(/{DESCRIPTION}/g, singleApp.description);
|
||||
source = source.replace(/{DESCRIPTION}/g, appMeta.description);
|
||||
} catch (error) {
|
||||
/* If the app doesn't have a README we fallback to the package description */
|
||||
console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
|
||||
source = singleApp.description;
|
||||
source = appMeta.description;
|
||||
}
|
||||
|
||||
const result = matter(source);
|
||||
@@ -111,8 +146,8 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
||||
data.items = data.items.map((item) => {
|
||||
if (typeof item === "string") {
|
||||
return getAppAssetFullPath(item, {
|
||||
dirName: singleApp.dirName,
|
||||
isTemplate: singleApp.isTemplate,
|
||||
dirName: appMeta.dirName,
|
||||
isTemplate: appMeta.isTemplate,
|
||||
});
|
||||
}
|
||||
return item;
|
||||
@@ -120,8 +155,9 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
isAppDisabled: false as const,
|
||||
source: { content, data },
|
||||
data: singleApp,
|
||||
data: appMeta,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -172,7 +172,7 @@ export default function Login({
|
||||
className="mb-0"
|
||||
{...register("password")}
|
||||
/>
|
||||
<div className="absolute -top-1.5 ltr:right-0 rtl:left-0">
|
||||
<div className="absolute -top-[2px] ltr:right-0 rtl:left-0">
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
tabIndex={-1}
|
||||
|
||||
@@ -157,10 +157,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
};
|
||||
}
|
||||
|
||||
let deploymentKey = await getDeploymentKey(prisma);
|
||||
const deploymentKey = await prisma.deployment.findUnique({
|
||||
where: { id: 1 },
|
||||
select: { licenseKey: true },
|
||||
});
|
||||
|
||||
// Check existant CALCOM_LICENSE_KEY env var and acccount for it
|
||||
if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey) {
|
||||
if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey?.licenseKey) {
|
||||
await prisma.deployment.upsert({
|
||||
where: { id: 1 },
|
||||
update: {
|
||||
@@ -172,10 +175,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
agreedLicenseAt: new Date(),
|
||||
},
|
||||
});
|
||||
deploymentKey = await getDeploymentKey(prisma);
|
||||
}
|
||||
|
||||
const isFreeLicense = deploymentKey === "";
|
||||
const isFreeLicense = (await getDeploymentKey(prisma)) === "";
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CheckIcon, MailOpenIcon, ExclamationIcon } from "@heroicons/react/outline";
|
||||
import { signIn } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -9,6 +8,7 @@ import z from "zod";
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
import { Check, MailOpen, AlertTriangle } from "@calcom/ui/components/icon";
|
||||
|
||||
import Loader from "@components/Loader";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
@@ -118,11 +118,11 @@ export default function Verify() {
|
||||
<div className="m-10 flex max-w-2xl flex-col items-start border border-white p-12 text-left">
|
||||
<div className="rounded-full border border-white p-3">
|
||||
{hasPaymentFailed ? (
|
||||
<ExclamationIcon className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
||||
<AlertTriangle className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
||||
) : sessionId ? (
|
||||
<CheckIcon className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
||||
<Check className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
||||
) : (
|
||||
<MailOpenIcon className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
||||
<MailOpen className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
||||
)}
|
||||
</div>
|
||||
<h3 className="font-cal my-6 text-3xl font-normal">
|
||||
|
||||
@@ -62,7 +62,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
||||
<h3 className="text-emphasis font-medium leading-6">
|
||||
{t("date_overrides")}{" "}
|
||||
<Tooltip content={t("date_overrides_info")}>
|
||||
<span className="inline-block">
|
||||
<span className="inline-block align-middle">
|
||||
<Info className="h-4 w-4" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -212,7 +212,7 @@ export default function Availability() {
|
||||
aria-label={t("delete")}
|
||||
className="hidden sm:inline"
|
||||
disabled={schedule?.isLastSchedule}
|
||||
tooltip={t("requires_at_least_one_schedule")}
|
||||
tooltip={schedule?.isLastSchedule ? t("requires_at_least_one_schedule") : t("delete")}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
<ConfirmationDialogContent
|
||||
|
||||
@@ -53,7 +53,7 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
||||
{t("overview_of_day")}{" "}
|
||||
<input
|
||||
type="date"
|
||||
className="inline h-8 border-none p-0"
|
||||
className="inline h-8 border-none bg-inherit p-0"
|
||||
defaultValue={formattedSelectedDate}
|
||||
onChange={(e) => {
|
||||
if (e.target.value) setSelectedDate(e.target.value);
|
||||
|
||||
@@ -443,8 +443,10 @@ export default function Success(props: SuccessProps) {
|
||||
)}
|
||||
{bookingInfo?.attendees.map((attendee) => (
|
||||
<div key={attendee.name + attendee.email} className="mb-3 last:mb-0">
|
||||
{attendee.name && <p>{attendee.name}</p>}
|
||||
<p data-testid={`attendee-${attendee.email}`}>{attendee.email}</p>
|
||||
{attendee.name && (
|
||||
<p data-testid={`attendee-name-${attendee.name}`}>{attendee.name}</p>
|
||||
)}
|
||||
<p data-testid={`attendee-email-${attendee.email}`}>{attendee.email}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -1092,6 +1094,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
|
||||
return {
|
||||
props: {
|
||||
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
|
||||
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
|
||||
profile,
|
||||
eventType,
|
||||
|
||||
@@ -160,6 +160,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
return {
|
||||
props: {
|
||||
away: user.away,
|
||||
themeBasis: user.username,
|
||||
isDynamicGroup: false,
|
||||
profile,
|
||||
date,
|
||||
|
||||
@@ -122,6 +122,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
return {
|
||||
props: {
|
||||
profile,
|
||||
themeBasis: user.username,
|
||||
eventType: eventTypeObject,
|
||||
booking: null,
|
||||
currentSlotBooking: null,
|
||||
|
||||
@@ -51,6 +51,7 @@ export type FormValues = {
|
||||
eventName: string;
|
||||
slug: string;
|
||||
length: number;
|
||||
offsetStart: number;
|
||||
description: string;
|
||||
disableGuests: boolean;
|
||||
requiresConfirmation: boolean;
|
||||
@@ -211,6 +212,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
bookingLimits: eventType.bookingLimits || undefined,
|
||||
durationLimits: eventType.durationLimits || undefined,
|
||||
length: eventType.length,
|
||||
offsetStart: eventType.offsetStart,
|
||||
hidden: eventType.hidden,
|
||||
periodDates: {
|
||||
startDate: periodDates.startDate,
|
||||
@@ -255,6 +257,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
)
|
||||
.optional(),
|
||||
length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
|
||||
offsetStart: z.union([z.string().transform((val) => +val), z.number()]).optional(),
|
||||
bookingFields: eventTypeBookingFields,
|
||||
})
|
||||
// TODO: Add schema for other fields later.
|
||||
|
||||
@@ -490,18 +490,20 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
{t("duplicate")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<EmbedButton
|
||||
as={DropdownItem}
|
||||
type="button"
|
||||
StartIcon={Code}
|
||||
className="w-full rounded-none"
|
||||
embedUrl={encodeURIComponent(embedLink)}>
|
||||
{t("embed")}
|
||||
</EmbedButton>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
{!isManagedEventType && (
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<EmbedButton
|
||||
as={DropdownItem}
|
||||
type="button"
|
||||
StartIcon={Code}
|
||||
className="w-full rounded-none"
|
||||
embedUrl={encodeURIComponent(embedLink)}>
|
||||
{t("embed")}
|
||||
</EmbedButton>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
|
||||
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) &&
|
||||
!isChildrenManagedEventType && (
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
|
||||
import { ConnectedVideoStep } from "@components/getting-started/steps-views/ConnectedVideoStep";
|
||||
import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability";
|
||||
import UserProfile from "@components/getting-started/steps-views/UserProfile";
|
||||
import { UserSettings } from "@components/getting-started/steps-views/UserSettings";
|
||||
@@ -22,7 +23,13 @@ import { UserSettings } from "@components/getting-started/steps-views/UserSettin
|
||||
export type IOnboardingPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
const INITIAL_STEP = "user-settings";
|
||||
const steps = ["user-settings", "connected-calendar", "setup-availability", "user-profile"] as const;
|
||||
const steps = [
|
||||
"user-settings",
|
||||
"connected-calendar",
|
||||
"connected-video",
|
||||
"setup-availability",
|
||||
"user-profile",
|
||||
] as const;
|
||||
|
||||
const stepTransform = (step: (typeof steps)[number]) => {
|
||||
const stepIndex = steps.indexOf(step);
|
||||
@@ -36,9 +43,9 @@ const stepRouteSchema = z.object({
|
||||
step: z.array(z.enum(steps)).default([INITIAL_STEP]),
|
||||
});
|
||||
|
||||
// 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
|
||||
const OnboardingPage = (props: IOnboardingPageProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { user } = props;
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -55,6 +62,11 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
|
||||
subtitle: [`${t("connect_your_calendar_instructions")}`],
|
||||
skipText: `${t("connect_calendar_later")}`,
|
||||
},
|
||||
{
|
||||
title: `${t("connect_your_video_app")}`,
|
||||
subtitle: [`${t("connect_your_video_app_instructions")}`],
|
||||
skipText: `${t("set_up_later")}`,
|
||||
},
|
||||
{
|
||||
title: `${t("set_availability")}`,
|
||||
subtitle: [
|
||||
@@ -68,6 +80,17 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
|
||||
},
|
||||
];
|
||||
|
||||
// TODO: Add this in when we have solved the ability to move to tokens accept invite and note invitedto
|
||||
// Ability to accept other pending invites if any (low priority)
|
||||
// if (props.hasPendingInvites) {
|
||||
// headers.unshift(
|
||||
// props.hasPendingInvites && {
|
||||
// title: `${t("email_no_user_invite_heading", { appName: APP_NAME })}`,
|
||||
// subtitle: [], // TODO: come up with some subtitle text here
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
|
||||
const goToIndex = (index: number) => {
|
||||
const newStep = steps[index];
|
||||
router.push(
|
||||
@@ -122,12 +145,15 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
|
||||
|
||||
{currentStep === "connected-calendar" && <ConnectedCalendars nextStep={() => goToIndex(2)} />}
|
||||
|
||||
{currentStep === "connected-video" && <ConnectedVideoStep nextStep={() => goToIndex(3)} />}
|
||||
|
||||
{currentStep === "setup-availability" && (
|
||||
<SetupAvailability nextStep={() => goToIndex(3)} defaultScheduleId={user.defaultScheduleId} />
|
||||
<SetupAvailability nextStep={() => goToIndex(4)} defaultScheduleId={user.defaultScheduleId} />
|
||||
)}
|
||||
|
||||
{currentStep === "user-profile" && <UserProfile user={user} />}
|
||||
</StepCard>
|
||||
|
||||
{headers[currentStepIndex]?.skipText && (
|
||||
<div className="flex w-full flex-row justify-center">
|
||||
<Button
|
||||
@@ -181,6 +207,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
allowDynamicBooking: true,
|
||||
defaultScheduleId: true,
|
||||
completedOnboarding: true,
|
||||
teams: {
|
||||
select: {
|
||||
accepted: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
logo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -199,6 +237,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
...user,
|
||||
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
||||
},
|
||||
hasPendingInvites: user.teams.find((team) => team.accepted === false) ?? false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Booker } from "@calcom/atoms";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
@@ -13,10 +14,22 @@ import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
export default function Type({ slug, user, booking, away }: PageProps) {
|
||||
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
|
||||
return (
|
||||
<main className="flex h-full min-h-[100dvh] items-center justify-center">
|
||||
<Booker username={user} eventSlug={slug} rescheduleBooking={booking} isAway={away} />
|
||||
<BookerSeo
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
rescheduleUid={booking?.uid}
|
||||
hideBranding={isBrandingHidden}
|
||||
/>
|
||||
<Booker
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
rescheduleBooking={booking}
|
||||
isAway={away}
|
||||
hideBranding={isBrandingHidden}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -27,8 +40,8 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
const { user, type: slug } = paramsSchema.parse(context.params);
|
||||
const { rescheduleUid } = context.query;
|
||||
|
||||
const { ssgInit } = await import("@server/lib/ssg");
|
||||
const ssg = await ssgInit(context);
|
||||
const { ssrInit } = await import("@server/lib/ssr");
|
||||
const ssr = await ssrInit(context);
|
||||
const usernameList = getUsernameList(user);
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
@@ -53,13 +66,16 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
|
||||
}
|
||||
|
||||
await ssr.viewer.public.event.prefetch({ username: user, eventSlug: slug });
|
||||
|
||||
return {
|
||||
props: {
|
||||
booking,
|
||||
user,
|
||||
slug,
|
||||
away: false,
|
||||
trpcState: ssg.dehydrate(),
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -67,14 +83,16 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
const { user: username, type: slug } = paramsSchema.parse(context.params);
|
||||
const { rescheduleUid } = context.query;
|
||||
const { ssgInit } = await import("@server/lib/ssg");
|
||||
const ssg = await ssgInit(context);
|
||||
|
||||
const { ssrInit } = await import("@server/lib/ssr");
|
||||
const ssr = await ssrInit(context);
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
select: {
|
||||
away: true,
|
||||
hideBranding: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -89,13 +107,16 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
|
||||
}
|
||||
|
||||
await ssr.viewer.public.event.prefetch({ username, eventSlug: slug });
|
||||
|
||||
return {
|
||||
props: {
|
||||
booking,
|
||||
away: user?.away,
|
||||
user: username,
|
||||
slug,
|
||||
trpcState: ssg.dehydrate(),
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: user?.hideBranding,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Booker } from "@calcom/atoms";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -12,10 +13,22 @@ import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
export default function Type({ slug, user, booking, away }: PageProps) {
|
||||
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
|
||||
return (
|
||||
<main className="flex h-full min-h-[100dvh] items-center justify-center">
|
||||
<Booker username={user} eventSlug={slug} rescheduleBooking={booking} isAway={away} />
|
||||
<BookerSeo
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
rescheduleUid={booking?.uid}
|
||||
hideBranding={isBrandingHidden}
|
||||
/>
|
||||
<Booker
|
||||
username={user}
|
||||
eventSlug={slug}
|
||||
rescheduleBooking={booking}
|
||||
isAway={away}
|
||||
hideBranding={isBrandingHidden}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -30,8 +43,8 @@ const paramsSchema = z.object({ type: z.string(), slug: z.string() });
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
|
||||
const { rescheduleUid } = context.query;
|
||||
const { ssgInit } = await import("@server/lib/ssg");
|
||||
const ssg = await ssgInit(context);
|
||||
const { ssrInit } = await import("@server/lib/ssr");
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
@@ -39,6 +52,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
hideBranding: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -52,14 +66,15 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
if (rescheduleUid) {
|
||||
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
|
||||
}
|
||||
|
||||
await ssr.viewer.public.event.prefetch({ username: teamSlug, eventSlug: meetingSlug });
|
||||
return {
|
||||
props: {
|
||||
booking,
|
||||
away: false,
|
||||
user: teamSlug,
|
||||
slug: meetingSlug,
|
||||
trpcState: ssg.dehydrate(),
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: team?.hideBranding,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -50,6 +50,7 @@ const ProfileImpersonationView = () => {
|
||||
setValue,
|
||||
reset,
|
||||
getValues,
|
||||
watch,
|
||||
} = formMethods;
|
||||
|
||||
const isDisabled = isSubmitting || !isDirty;
|
||||
@@ -63,11 +64,11 @@ const ProfileImpersonationView = () => {
|
||||
}}>
|
||||
<div className="flex space-x-3">
|
||||
<Switch
|
||||
defaultChecked={!user?.disableImpersonation}
|
||||
onCheckedChange={(e) => {
|
||||
setValue("disableImpersonation", !e, { shouldDirty: true });
|
||||
}}
|
||||
fitToHeight={true}
|
||||
checked={!watch("disableImpersonation")}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<Skeleton as={Label} className="text-emphasis text-sm font-semibold leading-none">
|
||||
|
||||
+40
-28
@@ -4,10 +4,12 @@ import { useRouter } from "next/router";
|
||||
import type { CSSProperties } from "react";
|
||||
import type { SubmitHandler } from "react-hook-form";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername";
|
||||
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -16,7 +18,6 @@ import { Alert, Button, EmailField, HeadSeo, PasswordField, TextField } from "@c
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import { asStringOrNull } from "../lib/asStringOrNull";
|
||||
import { IS_GOOGLE_LOGIN_ENABLED } from "../server/lib/constants";
|
||||
import { ssrInit } from "../server/lib/ssr";
|
||||
|
||||
@@ -24,11 +25,10 @@ type FormValues = {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
passwordcheck: string;
|
||||
apiError: string;
|
||||
};
|
||||
|
||||
export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof getServerSideProps>) {
|
||||
export default function Signup({ prepopulateFormValues, token }: inferSSRProps<typeof getServerSideProps>) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const telemetry = useTelemetry();
|
||||
@@ -76,7 +76,7 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<div
|
||||
className="bg-muted flex min-h-screen flex-col justify-center py-12 sm:px-6 lg:px-8"
|
||||
className="bg-muted flex min-h-screen flex-col justify-center "
|
||||
style={
|
||||
{
|
||||
"--cal-brand": "#111827",
|
||||
@@ -95,7 +95,7 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="bg-default mx-2 px-4 py-8 shadow sm:rounded-lg sm:px-10">
|
||||
<div className="bg-default mx-2 p-6 shadow sm:rounded-lg lg:p-8">
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
@@ -109,7 +109,7 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
|
||||
}}
|
||||
className="bg-default space-y-6">
|
||||
{errors.apiError && <Alert severity="error" message={errors.apiError?.message} />}
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-4">
|
||||
<TextField
|
||||
addOnLeading={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/`}
|
||||
{...register("username")}
|
||||
@@ -125,32 +125,28 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
|
||||
className: "block text-sm font-medium text-default",
|
||||
}}
|
||||
{...register("password")}
|
||||
hintErrors={["caplow", "min", "num"]}
|
||||
className="border-default mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:border-black focus:outline-none focus:ring-black sm:text-sm"
|
||||
/>
|
||||
<PasswordField
|
||||
label={t("confirm_password")}
|
||||
{...register("passwordcheck", {
|
||||
validate: (value) =>
|
||||
value === methods.watch("password") || (t("error_password_mismatch") as string),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex space-x-2 rtl:space-x-reverse">
|
||||
<Button type="submit" loading={isSubmitting} className="w-7/12 justify-center">
|
||||
<Button type="submit" loading={isSubmitting} className="w-full justify-center">
|
||||
{t("create_account")}
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
className="w-5/12 justify-center"
|
||||
onClick={() =>
|
||||
signIn("Cal.com", {
|
||||
callbackUrl: router.query.callbackUrl
|
||||
? `${WEBAPP_URL}/${router.query.callbackUrl}`
|
||||
: `${WEBAPP_URL}/getting-started`,
|
||||
})
|
||||
}>
|
||||
{t("login_instead")}
|
||||
</Button>
|
||||
{!token && (
|
||||
<Button
|
||||
color="secondary"
|
||||
className="w-full justify-center"
|
||||
onClick={() =>
|
||||
signIn("Cal.com", {
|
||||
callbackUrl: router.query.callbackUrl
|
||||
? `${WEBAPP_URL}/${router.query.callbackUrl}`
|
||||
: `${WEBAPP_URL}/getting-started`,
|
||||
})
|
||||
}>
|
||||
{t("login_instead")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
@@ -163,7 +159,7 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
|
||||
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
const ssr = await ssrInit(ctx);
|
||||
const token = asStringOrNull(ctx.query.token);
|
||||
const token = z.string().optional().parse(ctx.query.token);
|
||||
|
||||
const props = {
|
||||
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
|
||||
@@ -221,11 +217,27 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
};
|
||||
}
|
||||
|
||||
const guessUsernameFromEmail = (email: string) => {
|
||||
const [username] = email.split("@");
|
||||
return username;
|
||||
};
|
||||
|
||||
let username = guessUsernameFromEmail(verificationToken.identifier);
|
||||
|
||||
if (!IS_SELF_HOSTED) {
|
||||
// Im not sure we actually hit this because of next redirects signup to website repo - but just in case this is pretty cool :)
|
||||
const { available, suggestion } = await checkPremiumUsername(username);
|
||||
|
||||
username = available ? username : suggestion || username;
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
...props,
|
||||
token,
|
||||
prepopulateFormValues: {
|
||||
email: verificationToken.identifier,
|
||||
username,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
@@ -12,6 +12,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { Avatar, AvatarGroup, Button, EmptyScreen, HeadSeo } from "@calcom/ui";
|
||||
@@ -26,7 +27,7 @@ import Team from "@components/team/screens/Team";
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export type TeamPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
||||
function TeamPage({ team, isUnpublished, markdownStrippedBio }: TeamPageProps) {
|
||||
useTheme(team.theme);
|
||||
const showMembers = useToggleQuery("members");
|
||||
const { t } = useLocale();
|
||||
@@ -67,6 +68,11 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
||||
<div className="px-6 py-4 ">
|
||||
<Link
|
||||
href={`/team/${team.slug}/${type.slug}`}
|
||||
onClick={async () => {
|
||||
sdkActionManager?.fire("eventTypeSelected", {
|
||||
eventType: type,
|
||||
});
|
||||
}}
|
||||
data-testid="event-type-link"
|
||||
className="flex justify-between">
|
||||
<div className="flex-shrink">
|
||||
@@ -100,7 +106,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
||||
title={teamName}
|
||||
description={teamName}
|
||||
meeting={{
|
||||
title: team?.bio || "",
|
||||
title: markdownStrippedBio,
|
||||
profile: { name: `${team.name}`, image: getPlaceholderAvatar(team.logo, team.name) },
|
||||
}}
|
||||
/>
|
||||
@@ -111,7 +117,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
||||
{!isBioEmpty && (
|
||||
<>
|
||||
<div
|
||||
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: team.safeBio }}
|
||||
/>
|
||||
</>
|
||||
@@ -196,10 +202,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
return { ...member, safeBio: markdownToSafeHTML(member.bio || "") };
|
||||
});
|
||||
|
||||
const markdownStrippedBio = stripMarkdown(team?.bio || "");
|
||||
|
||||
return {
|
||||
props: {
|
||||
team: { ...team, safeBio, members },
|
||||
themeBasis: team.slug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
markdownStrippedBio,
|
||||
},
|
||||
} as const;
|
||||
};
|
||||
|
||||
@@ -196,6 +196,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
brandColor: team.brandColor,
|
||||
darkBrandColor: team.darkBrandColor,
|
||||
},
|
||||
themeBasis: team.slug,
|
||||
date: dateParam,
|
||||
eventType: eventTypeObject,
|
||||
workingHours,
|
||||
|
||||
@@ -153,6 +153,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
image: eventTypeObject.team?.logo || null,
|
||||
eventName: null,
|
||||
},
|
||||
themeBasis: eventTypeObject.team?.slug,
|
||||
eventType: eventTypeObject,
|
||||
recurringEventCount,
|
||||
booking,
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
bookTimeSlot,
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
selectSecondAvailableTimeSlotNextMonth,
|
||||
testEmail,
|
||||
testName,
|
||||
} from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
@@ -101,6 +103,8 @@ testBothBookers.describe("pro user", () => {
|
||||
// Because it tests the entire booking flow + the cancellation + rebooking
|
||||
test.setTimeout(testInfo.timeout * 3);
|
||||
await bookFirstEvent(page);
|
||||
await expect(page.locator(`[data-testid="attendee-email-${testEmail}"]`)).toHaveText(testEmail);
|
||||
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
|
||||
|
||||
const [pro] = users.get();
|
||||
await pro.login();
|
||||
@@ -114,7 +118,10 @@ testBothBookers.describe("pro user", () => {
|
||||
|
||||
const cancelledHeadline = await page.locator('[data-testid="cancelled-headline"]').innerText();
|
||||
|
||||
await expect(cancelledHeadline).toBe("This event is cancelled");
|
||||
expect(cancelledHeadline).toBe("This event is cancelled");
|
||||
|
||||
await expect(page.locator(`[data-testid="attendee-email-${testEmail}"]`)).toHaveText(testEmail);
|
||||
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
|
||||
|
||||
await page.goto(`/${pro.username}`);
|
||||
await bookFirstEvent(page);
|
||||
@@ -155,7 +162,7 @@ testBothBookers.describe("pro user", () => {
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
additionalGuests.forEach(async (email) => {
|
||||
await expect(page.locator(`[data-testid="attendee-${email}"]`)).toHaveText(email);
|
||||
await expect(page.locator(`[data-testid="attendee-email-${email}"]`)).toHaveText(email);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -419,9 +419,13 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
|
||||
// Go to cancel page and see that attendees are listed and myself as I'm owner of the booking
|
||||
await page.goto(`/booking/${booking.uid}?cancel=true&allRemainingBookings=false`);
|
||||
|
||||
const foundFirstAttendeeAsOwner = await page.locator('p[data-testid="attendee-first+seats@cal.com"]');
|
||||
const foundFirstAttendeeAsOwner = await page.locator(
|
||||
'p[data-testid="attendee-email-first+seats@cal.com"]'
|
||||
);
|
||||
await expect(foundFirstAttendeeAsOwner).toHaveCount(1);
|
||||
const foundSecondAttendeeAsOwner = await page.locator('p[data-testid="attendee-second+seats@cal.com"]');
|
||||
const foundSecondAttendeeAsOwner = await page.locator(
|
||||
'p[data-testid="attendee-email-second+seats@cal.com"]'
|
||||
);
|
||||
await expect(foundSecondAttendeeAsOwner).toHaveCount(1);
|
||||
await page.pause();
|
||||
await page.goto("auth/logout");
|
||||
@@ -433,10 +437,12 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
|
||||
);
|
||||
|
||||
// No attendees should be displayed only the one that it's cancelling
|
||||
const notFoundSecondAttendee = await page.locator('p[data-testid="attendee-second+seats@cal.com"]');
|
||||
const notFoundSecondAttendee = await page.locator(
|
||||
'p[data-testid="attendee-email-second+seats@cal.com"]'
|
||||
);
|
||||
|
||||
await expect(notFoundSecondAttendee).toHaveCount(0);
|
||||
const foundFirstAttendee = await page.locator('p[data-testid="attendee-first+seats@cal.com"]');
|
||||
const foundFirstAttendee = await page.locator('p[data-testid="attendee-email-first+seats@cal.com"]');
|
||||
await expect(foundFirstAttendee).toHaveCount(1);
|
||||
|
||||
await prisma.eventType.update({
|
||||
@@ -453,11 +459,11 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
|
||||
);
|
||||
|
||||
// Now attendees should be displayed
|
||||
const foundSecondAttendee = await page.locator('p[data-testid="attendee-second+seats@cal.com"]');
|
||||
const foundSecondAttendee = await page.locator('p[data-testid="attendee-email-second+seats@cal.com"]');
|
||||
|
||||
await expect(foundSecondAttendee).toHaveCount(1);
|
||||
const foundFirstAttendeeAgain = await page
|
||||
.locator('p[data-testid="attendee-first+seats@cal.com"]')
|
||||
.locator('p[data-testid="attendee-email-first+seats@cal.com"]')
|
||||
.first();
|
||||
await expect(foundFirstAttendeeAgain).toHaveCount(1);
|
||||
});
|
||||
|
||||
@@ -15,6 +15,9 @@ type Request = IncomingMessage & { body?: unknown };
|
||||
type RequestHandlerOptions = { req: Request; res: ServerResponse };
|
||||
type RequestHandler = (opts: RequestHandlerOptions) => void;
|
||||
|
||||
export const testEmail = "test@example.com";
|
||||
export const testName = "Test Testson";
|
||||
|
||||
export function createHttpServer(opts: { requestHandler?: RequestHandler } = {}) {
|
||||
const {
|
||||
requestHandler = ({ res }) => {
|
||||
@@ -133,8 +136,8 @@ export async function bookFirstEvent(page: Page) {
|
||||
|
||||
export const bookTimeSlot = async (page: Page, opts?: { name?: string; email?: string }) => {
|
||||
// --- fill form
|
||||
await page.fill('[name="name"]', opts?.name ?? "Test Testson");
|
||||
await page.fill('[name="email"]', opts?.email ?? "test@example.com");
|
||||
await page.fill('[name="name"]', opts?.name ?? testName);
|
||||
await page.fill('[name="email"]', opts?.email ?? testEmail);
|
||||
await page.press('[name="email"]', "Enter");
|
||||
};
|
||||
// Provide an standalone localize utility not managed by next-i18n
|
||||
|
||||
@@ -47,10 +47,19 @@ test.describe("Onboarding", () => {
|
||||
// tests skip button, we don't want to test entire flow.
|
||||
await page.locator("button[data-testid=skip-step]").click();
|
||||
|
||||
await expect(page).toHaveURL(/.*setup-availability/);
|
||||
await expect(page).toHaveURL(/.*connected-video/);
|
||||
});
|
||||
|
||||
await test.step("step 3", async () => {
|
||||
const isDisabled = await page.locator("button[data-testid=save-video-button]").isDisabled();
|
||||
await expect(isDisabled).toBe(true);
|
||||
// tests skip button, we don't want to test entire flow.
|
||||
await page.locator("button[data-testid=skip-step]").click();
|
||||
|
||||
await expect(page).toHaveURL(/.*setup-availability/);
|
||||
});
|
||||
|
||||
await test.step("step 4", async () => {
|
||||
const isDisabled = await page.locator("button[data-testid=save-availability]").isDisabled();
|
||||
await expect(isDisabled).toBe(false);
|
||||
// same here, skip this step.
|
||||
@@ -59,7 +68,7 @@ test.describe("Onboarding", () => {
|
||||
await expect(page).toHaveURL(/.*user-profile/);
|
||||
});
|
||||
|
||||
await test.step("step 4", async () => {
|
||||
await test.step("step 5", async () => {
|
||||
await page.locator("button[type=submit]").click();
|
||||
|
||||
// should redirect to /event-types after onboarding
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 485 B After Width: | Height: | Size: 743 B |
@@ -1,3 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke="white" stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ban"><circle cx="12" cy="12" r="10"></circle><line x1="4.93" x2="19.07" y1="4.93" y2="19.07"></line></svg>
|
||||
|
Before Width: | Height: | Size: 309 B After Width: | Height: | Size: 308 B |
@@ -1572,7 +1572,7 @@
|
||||
"sender_name": "اسم المرسل",
|
||||
"already_invited": "تمت دعوة هذا الحاضر بالفعل",
|
||||
"no_recordings_found": "لم يتم العثور على تسجيلات",
|
||||
"new_workflow_subtitle": "سير عمل جديد من أجل...",
|
||||
"new_workflow_subtitle": "سير عمل جديد لـ...",
|
||||
"reporting": "تقديم التقارير",
|
||||
"reporting_feature": "عرض جميع البيانات الواردة وتنزيلها كملف CSV",
|
||||
"teams_plan_required": "خطة Teams مطلوبة",
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "سيكون الأعضاء قادرين على رؤية التطبيقات النشطة، ولكن من دون القدرة على تعديل أي إعدادات للتطبيق",
|
||||
"locked_webhooks_description": "سيكون الأعضاء قادرين على رؤية قوالب الويب النشطة، ولكن من دون القدرة على تعديل أي إعدادات للقوالب",
|
||||
"locked_workflows_description": "سيكون الأعضاء قادرين على رؤية مسارات العمل النشطة، ولكن من دون القدرة على تعديل أي إعدادات لمسار العمل",
|
||||
"locked_by_admin": "مقفل من قبل المشرف",
|
||||
"app_not_connected": "أنت غير متصل بحساب {{appName}}.",
|
||||
"connect_now": "اتصل الآن",
|
||||
"managed_event_dialog_confirm_button_one": "استبدال وإشعار العضو {{count}}",
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "Členové budou vidět aktivní aplikace, ale nastavení aplikací upravovat moci nebudou",
|
||||
"locked_webhooks_description": "Členové budou vidět aktivní webhooky, ale nastavení webhooků upravovat moci nebudou",
|
||||
"locked_workflows_description": "Členové budou vidět aktivní pracovní postupy, ale nastavení pracovních postupů upravovat moci nebudou",
|
||||
"locked_by_admin": "Uzamčeno správcem",
|
||||
"app_not_connected": "Nemáte připojený účet {{appName}}.",
|
||||
"connect_now": "Připojte se",
|
||||
"managed_event_dialog_confirm_button_one": "Nahradit a upozornit {{count}} člena",
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "Mitglieder können die aktiven Apps sehen, können aber keine App-Einstellungen anpassen",
|
||||
"locked_webhooks_description": "Mitglieder können die aktiven Webhooks sehen, können aber keine Webhook-Einstellungen anpassen",
|
||||
"locked_workflows_description": "Mitglieder können die aktiven Workflows sehen, können aber keine Workflow-Einstellungen anpassen",
|
||||
"locked_by_admin": "Von Admin gesperrt",
|
||||
"app_not_connected": "Sie haben kein {{appName}}-Konto verbunden.",
|
||||
"connect_now": "Jetzt verbinden",
|
||||
"managed_event_dialog_confirm_button_one": "{{count}} Mitglied ersetzen & benachrichtigen",
|
||||
|
||||
@@ -237,6 +237,8 @@
|
||||
"set_availability": "Set your availability",
|
||||
"continue_without_calendar": "Continue without calendar",
|
||||
"connect_your_calendar": "Connect your calendar",
|
||||
"connect_your_video_app":"Connect your video apps",
|
||||
"connect_your_video_app_instructions":"Connect your video apps to use them on your event types.",
|
||||
"connect_your_calendar_instructions": "Connect your calendar to automatically check for busy times and new events as they’re scheduled.",
|
||||
"set_up_later": "Set up later",
|
||||
"current_time": "Current time",
|
||||
@@ -367,6 +369,7 @@
|
||||
"create_webhook": "Create Webhook",
|
||||
"booking_cancelled": "Booking Cancelled",
|
||||
"booking_rescheduled": "Booking Rescheduled",
|
||||
"recording_ready":"Recording Download Link Ready",
|
||||
"booking_created": "Booking Created",
|
||||
"meeting_ended": "Meeting Ended",
|
||||
"form_submitted": "Form Submitted",
|
||||
@@ -753,6 +756,10 @@
|
||||
"new_event_type_to_book_description": "Create a new event type for people to book times with.",
|
||||
"length": "Length",
|
||||
"minimum_booking_notice": "Minimum Notice",
|
||||
"offset_toggle": "Offset start times",
|
||||
"offset_toggle_description": "Offset timeslots shown to bookers by a specified number of minutes",
|
||||
"offset_start": "Offset by",
|
||||
"offset_start_description": "e.g. this will show time slots to your bookers at {{ adjustedTime }} instead of {{ originalTime }}",
|
||||
"slot_interval": "Time-slot intervals",
|
||||
"slot_interval_default": "Use event length (default)",
|
||||
"delete_event_type": "Delete event type?",
|
||||
@@ -1192,6 +1199,7 @@
|
||||
"create_workflow": "Create a workflow",
|
||||
"do_this": "Do this",
|
||||
"turn_off": "Turn off",
|
||||
"turn_on": "Turn on",
|
||||
"settings_updated_successfully": "Settings updated successfully",
|
||||
"error_updating_settings": "Error updating settings",
|
||||
"personal_cal_url": "My personal {{appName}} URL",
|
||||
@@ -1623,6 +1631,7 @@
|
||||
"email_user_cta": "View Invitation",
|
||||
"email_no_user_invite_heading": "You’ve been invited to join a team on {{appName}}",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}} has invited you to join their team on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
|
||||
"email_user_invite_subheading": "{{invitedBy}} has invited you to join their team `{{teamName}}` on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
|
||||
"email_no_user_invite_steps_intro": "We’ll walk you through a few short steps and you’ll be enjoying stress free scheduling with your team in no time.",
|
||||
"email_no_user_step_one": "Choose your username",
|
||||
"email_no_user_step_two": "Connect your calendar account",
|
||||
@@ -1736,7 +1745,7 @@
|
||||
"locked_apps_description": "Members will be able to see the active apps but will not be able to edit any app settings",
|
||||
"locked_webhooks_description": "Members will be able to see the active webhooks but will not be able to edit any webhook settings",
|
||||
"locked_workflows_description": "Members will be able to see the active workflows but will not be able to edit any workflow settings",
|
||||
"locked_by_admin": "Locked by admin",
|
||||
"locked_by_admin": "Locked by team admin",
|
||||
"app_not_connected": "You have not connected a {{appName}} account.",
|
||||
"connect_now": "Connect now",
|
||||
"managed_event_dialog_confirm_button_one": "Replace & notify {{count}} member",
|
||||
@@ -1810,6 +1819,8 @@
|
||||
"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",
|
||||
"form_updated_successfully":"Form updated successfully.",
|
||||
"email_not_cal_member_cta": "Join your team",
|
||||
"disable_attendees_confirmation_emails": "Disable default confirmation emails for attendees",
|
||||
"disable_attendees_confirmation_emails_description": "At least one workflow is active on this event type that sends an email to the attendees when the event is booked.",
|
||||
"disable_host_confirmation_emails": "Disable default confirmation emails for host",
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "Los miembros podrán ver las aplicaciones activas pero no podrán editar ninguna configuración de la aplicación",
|
||||
"locked_webhooks_description": "Los miembros podrán ver los webhooks activos, pero no podrán editar ninguna configuración de webhooks",
|
||||
"locked_workflows_description": "Los miembros podrán ver los flujos de trabajo activos, pero no podrán editar ninguna configuración de flujo de trabajo",
|
||||
"locked_by_admin": "Bloqueado por el administrador",
|
||||
"app_not_connected": "No ha conectado una cuenta de {{appName}}.",
|
||||
"connect_now": "Conectar ahora",
|
||||
"managed_event_dialog_confirm_button_one": "Reemplazar y notificar a {{count}} miembro",
|
||||
|
||||
@@ -237,6 +237,8 @@
|
||||
"set_availability": "Définissez vos disponibilités",
|
||||
"continue_without_calendar": "Continuer sans calendrier",
|
||||
"connect_your_calendar": "Connectez votre calendrier",
|
||||
"connect_your_video_app": "Connectez vos applications vidéo",
|
||||
"connect_your_video_app_instructions": "Connectez vos applications vidéo pour les utiliser sur vos types d'événements.",
|
||||
"connect_your_calendar_instructions": "Connectez votre calendrier pour vérifier automatiquement vos disponibilités ainsi que les nouveaux événements lorsqu'ils sont planifiés.",
|
||||
"set_up_later": "Configurer plus tard",
|
||||
"current_time": "Heure actuelle",
|
||||
@@ -367,6 +369,7 @@
|
||||
"create_webhook": "Créer un Webhook",
|
||||
"booking_cancelled": "Réservation annulée",
|
||||
"booking_rescheduled": "Réservation replanifiée",
|
||||
"recording_ready": "Lien de téléchargement d'enregistrement prêt",
|
||||
"booking_created": "Réservation créée",
|
||||
"meeting_ended": "Rendez-vous terminé",
|
||||
"form_submitted": "Formulaire envoyé",
|
||||
@@ -1192,6 +1195,7 @@
|
||||
"create_workflow": "Créer un workflow",
|
||||
"do_this": "Effectuer ceci",
|
||||
"turn_off": "Désactiver",
|
||||
"turn_on": "Activer",
|
||||
"settings_updated_successfully": "Paramètres mis à jour avec succès",
|
||||
"error_updating_settings": "Erreur lors de la mise à jour des paramètres",
|
||||
"personal_cal_url": "Mon lien {{appName}} personnel",
|
||||
@@ -1623,6 +1627,7 @@
|
||||
"email_user_cta": "Voir l'invitation",
|
||||
"email_no_user_invite_heading": "Vous avez été invité(e) à rejoindre une équipe sur {{appName}}",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}} a été invité à rejoindre son équipe sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet, à vous et à votre équipe, de planifier des rendez-vous sans les allers-retours par e-mail.",
|
||||
"email_user_invite_subheading": "{{invitedBy}} vous a invité à rejoindre son équipe « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet, à vous et à votre équipe, de planifier des réunions sans les allers-retours par e-mail.",
|
||||
"email_no_user_invite_steps_intro": "Nous vous guiderons à travers quelques étapes courtes et vous profiterez d'une planification sans stress avec votre équipe en un rien de temps.",
|
||||
"email_no_user_step_one": "Choisissez votre nom d'utilisateur",
|
||||
"email_no_user_step_two": "Connectez votre compte de calendrier",
|
||||
@@ -1736,7 +1741,7 @@
|
||||
"locked_apps_description": "Les membres pourront voir les applications actives, mais ne pourront pas modifier leurs paramètres.",
|
||||
"locked_webhooks_description": "Les membres pourront voir les webhooks actifs, mais ne pourront pas modifier leurs paramètres.",
|
||||
"locked_workflows_description": "Les membres pourront voir les workflows actifs, mais ne pourront pas modifier leurs paramètres.",
|
||||
"locked_by_admin": "Verrouillé par l'administrateur",
|
||||
"locked_by_admin": "Verrouillé par l'administrateur de l'équipe",
|
||||
"app_not_connected": "Vous n'avez pas connecté de compte {{appName}}.",
|
||||
"connect_now": "Connecter maintenant",
|
||||
"managed_event_dialog_confirm_button_one": "Remplacer et notifier {{count}} membre",
|
||||
@@ -1805,6 +1810,7 @@
|
||||
"inline_embed": "Intégration en ligne",
|
||||
"load_inline_content": "Charge votre type d'événement directement en ligne avec le contenu de votre site web.",
|
||||
"floating_pop_up_button": "Bouton pop-up flottant",
|
||||
"email_not_cal_member_cta": "Rejoignez votre équipe",
|
||||
"disable_attendees_confirmation_emails": "Désactiver les e-mails de confirmation par défaut pour les participants",
|
||||
"disable_attendees_confirmation_emails_description": "Au moins un workflow est actif sur ce type d'événement qui envoie un e-mail aux participants lorsque l'événement est réservé.",
|
||||
"disable_host_confirmation_emails": "Désactiver les e-mails de confirmation par défaut pour l'hôte",
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "I membri saranno in grado di vedere le app attive, ma non saranno in grado di modificare le impostazioni delle app",
|
||||
"locked_webhooks_description": "I membri saranno in grado di vedere i webhook attivi, ma non saranno in grado di modificare le impostazioni dei webhook",
|
||||
"locked_workflows_description": "I membri saranno in grado di vedere i flussi di lavoro attivi, ma non saranno in grado di modificare le impostazioni dei flussi di lavoro",
|
||||
"locked_by_admin": "Bloccato dall'amministratore",
|
||||
"app_not_connected": "Non è stato connesso un account di {{appName}}.",
|
||||
"connect_now": "Connetti ora",
|
||||
"managed_event_dialog_confirm_button_one": "Sostituisci e invia notifica a {{count}} membro",
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "メンバーは有効なアプリを確認できますが、アプリの設定を編集することはできません",
|
||||
"locked_webhooks_description": "メンバーは有効なウェブフックを確認できますが、ウェブフックの設定を編集することはできません",
|
||||
"locked_workflows_description": "メンバーは有効なワークフローを確認できますが、ワークフローの設定を編集することはできません",
|
||||
"locked_by_admin": "管理者によりロックされています",
|
||||
"app_not_connected": "{{appName}} のアカウントに接続していません。",
|
||||
"connect_now": "今すぐ接続",
|
||||
"managed_event_dialog_confirm_button_one": "{{count}} 人のメンバーを置き換えて通知する",
|
||||
|
||||
@@ -237,6 +237,7 @@
|
||||
"set_availability": "언제 여유가 있는지 설정하세요.",
|
||||
"continue_without_calendar": "캘린더 없이 계속하기",
|
||||
"connect_your_calendar": "캘린더 연결하기",
|
||||
"connect_your_video_app_instructions": "이벤트 타입에서 사용하기 위한 비디오 앱을 연결해주세요.",
|
||||
"connect_your_calendar_instructions": "바쁜 시간과 새 이벤트 일정을 자동으로 확인할 수 있도록 캘린더 연결",
|
||||
"set_up_later": "나중에 하기",
|
||||
"current_time": "현재 시각",
|
||||
@@ -465,6 +466,7 @@
|
||||
"friday": "금요일",
|
||||
"saturday": "토요일",
|
||||
"sunday": "일요일",
|
||||
"all_booked_today": "모두 예약됨.",
|
||||
"slots_load_fail": "사용 가능한 시간 슬롯을 로드할 수 없습니다.",
|
||||
"additional_guests": "게스트 추가",
|
||||
"your_name": "당신의 이름",
|
||||
@@ -1038,6 +1040,7 @@
|
||||
"event_cancelled_trigger": "이벤트 취소 시",
|
||||
"new_event_trigger": "새 이벤트 예약 시",
|
||||
"email_host_action": "호스트에게 이메일 발송",
|
||||
"email_attendee_action": "참석자들에게 이메일 전송",
|
||||
"sms_attendee_action": "참석자에게 SMS 발송",
|
||||
"sms_number_action": "특정 번호로 SMS 발송",
|
||||
"workflows": "워크플로",
|
||||
@@ -1246,6 +1249,7 @@
|
||||
"calendars_description": "이벤트 타입이 캘린더와 상호 작용하는 방식 구성",
|
||||
"appearance_description": "예약 모양 설정 관리",
|
||||
"conferencing_description": "회의에 즐겨 사용하는 화상 회의 앱 추가",
|
||||
"add_conferencing_app": "회의 앱 추가",
|
||||
"password_description": "계정 비밀번호 설정 관리",
|
||||
"2fa_description": "계정 비밀번호 설정 관리",
|
||||
"we_just_need_basic_info": "프로필 설정을 위해 몇 가지 기본 정보만 있으면 됩니다.",
|
||||
@@ -1620,6 +1624,7 @@
|
||||
"email_user_cta": "초대 보기",
|
||||
"email_no_user_invite_heading": "{{appName}} 팀에 초대되었습니다",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}} 님이 귀하를 {{appName}}에서 자신의 팀에 초대했습니다. {{appName}} 앱은 귀하와 귀하의 팀이 이메일을 주고 받지 않고 회의 일정을 잡을 수 있게 해주는 이벤트 정리 스케줄러입니다.",
|
||||
"email_user_invite_subheading": "{{invitedBy}}님이 {{appName}}에서 자신의 `{{teamName}}` 팀에 가입하도록 당신을 초대했습니다. {{appName}}은 유저와 팀이 이메일을 주고 받지 않고도 회의 일정을 잡을 수 있게 하는 이벤트 조율 스케줄러입니다.",
|
||||
"email_no_user_invite_steps_intro": "안내해 드릴 몇 가지 간단한 단계만 거치면 곧 팀과 함께 스트레스 없이 편리하게 일정을 관리할 수 있게 될 것입니다.",
|
||||
"email_no_user_step_one": "사용자 이름 선택",
|
||||
"email_no_user_step_two": "캘린더 계정 연결",
|
||||
@@ -1695,6 +1700,14 @@
|
||||
"spot_popular_event_types_description": "어떤 이벤트 타입이 가장 많은 클릭을 받고 예약이 되고 있는지 확인해보세요",
|
||||
"no_responses_yet": "아직 응답 없음",
|
||||
"this_will_be_the_placeholder": "이것은 자리 표시자가 됩니다",
|
||||
"error_booking_event": "이벤트를 예약하는 도중 오류가 발생했습니다. 페이지를 새로고침한 후 다시 시도해주세요.",
|
||||
"timeslot_missing_title": "시간대가 선택되지 않았습니다",
|
||||
"timeslot_missing_description": "이벤트를 예약 할 시간대를 선택해주세요.",
|
||||
"timeslot_missing_cta": "시간대 선택",
|
||||
"switch_monthly": "월별 보기로 전환",
|
||||
"switch_weekly": "주간 보기로 전환",
|
||||
"switch_multiday": "일간 보기로 전환",
|
||||
"num_locations": "{{num}}개의 위치 옵션",
|
||||
"this_meeting_has_not_started_yet": "이 회의는 아직 시작되지 않았습니다",
|
||||
"this_app_requires_connected_account": "{{appName}}에 연결된 {{dependencyName}} 계정이 필요합니다",
|
||||
"connect_app": "{{dependencyName}} 연결",
|
||||
@@ -1724,7 +1737,6 @@
|
||||
"locked_apps_description": "회원은 활성 앱을 볼 수 있지만 앱 설정을 편집할 수는 없습니다",
|
||||
"locked_webhooks_description": "회원은 활성 웹훅을 볼 수 있지만 웹훅 설정을 편집할 수는 없습니다",
|
||||
"locked_workflows_description": "회원은 활성 워크플로를 볼 수 있지만 워크플로 설정을 편집할 수는 없습니다",
|
||||
"locked_by_admin": "관리자에 의해 잠금 됨",
|
||||
"app_not_connected": "{{appName}} 계정을 연결하지 않으셨습니다.",
|
||||
"connect_now": "지금 연결",
|
||||
"managed_event_dialog_confirm_button_one": "회원 {{count}}명 바꾸기 및 알림",
|
||||
@@ -1782,5 +1794,15 @@
|
||||
"seats_and_no_show_fee_error": "현재 좌석을 활성화할 수 없으므로 노쇼 수수료를 부과할 수 없습니다",
|
||||
"complete_your_booking": "예약을 완료하세요",
|
||||
"complete_your_booking_subject": "예약 완료: {{title}} 날짜 {{date}}",
|
||||
"email_invite_team": "{{email}} 이 초대되었습니다."
|
||||
"currency_string": "{{amount, currency}}",
|
||||
"charge_card_dialog_body": "참석자에게 {{amount, currency}}을 청구하려고 합니다. 계속 진행하시겠습니까?",
|
||||
"charge_attendee": "참석자에게 {{amount, currency}}을 청구합니다.",
|
||||
"payment_app_commission": "결제가 필요합니다 (건당 수수료 {{paymentFeePercentage}}% + {{fee, currency}})",
|
||||
"email_invite_team": "{{email}} 이 초대되었습니다.",
|
||||
"image_size_limit_exceed": "업로드되는 이미지의 크기는 5MB를 초과하면 안 됩니다.",
|
||||
"need_help_embedding": "도움이 필요하신가요? Wix, Squarespace, 또는 WordPress에서 Cal을 임베드하는 가이드를 참조하거나, 자주 묻는 질문을 확인하거나, 고급 임베드 옵션을 살펴보세요.",
|
||||
"disable_attendees_confirmation_emails": "참석자에 대한 기본 확인 이메일 비활성화",
|
||||
"disable_attendees_confirmation_emails_description": "이 이벤트 유형에는 이벤트가 예약되었을 때 참석자들에게 이메일을 보내는 최소한 하나의 워크플로가 활성화되어 있습니다.",
|
||||
"disable_host_confirmation_emails": "호스트에 대한 기본 확인 이메일 비활성화",
|
||||
"disable_host_confirmation_emails_description": "이 이벤트 유형에는 이벤트가 예약될 때 호스트에게 이메일을 보내는 최소한 하나의 워크플로가 활성화되어 있습니다."
|
||||
}
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "Leden kunnen de actieve apps zien, maar kunnen geen appinstellingen bewerken",
|
||||
"locked_webhooks_description": "Leden kunnen de actieve webhooks zien, maar kunnen geen webhookinstellingen bewerken",
|
||||
"locked_workflows_description": "Leden kunnen de actieve werkstromen zien, maar kunnen geen werkstroominstellingen bewerken",
|
||||
"locked_by_admin": "Vergrendeld door beheerder",
|
||||
"app_not_connected": "U heeft geen {{appName}}-account gekoppeld.",
|
||||
"connect_now": "Nu koppelen",
|
||||
"managed_event_dialog_confirm_button_one": "{{count}} lid vervangen en informeren",
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "Członkowie będą mogli zobaczyć aktywne aplikacje, ale nie będą mogli zmieniać ich ustawień.",
|
||||
"locked_webhooks_description": "Członkowie będą mogli zobaczyć aktywne webhooki, ale nie będą mogli zmieniać ich ustawień.",
|
||||
"locked_workflows_description": "Członkowie będą mogli zobaczyć aktywne przepływy pracy, ale nie będą mogli zmieniać ich ustawień.",
|
||||
"locked_by_admin": "Zablokowane przez administratora",
|
||||
"app_not_connected": "Nie połączono konta aplikacji {{appName}}.",
|
||||
"connect_now": "Połącz teraz",
|
||||
"managed_event_dialog_confirm_button_one": "Zastąp i poinformuj {{count}} członka",
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "Os membros poderão ver os aplicativos ativos, mas não poderão editar as configurações",
|
||||
"locked_webhooks_description": "Os membros poderão ver os webhooks ativos, mas não poderão editar as configurações",
|
||||
"locked_workflows_description": "Os membros poderão ver os fluxos de trabalho ativos, mas não poderão editar as configurações",
|
||||
"locked_by_admin": "Bloqueado pelo administrador",
|
||||
"app_not_connected": "Você não conectou uma conta do {{appName}}.",
|
||||
"connect_now": "Conectar agora",
|
||||
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "Os membros poderão ver as aplicações ativas, mas não poderão editar quaisquer configurações das aplicações",
|
||||
"locked_webhooks_description": "Os membros poderão ver os webhooks ativos, mas não poderão editar quaisquer configurações dos webhooks",
|
||||
"locked_workflows_description": "Os membros poderão ver os fluxos de trabalho ativos, mas não poderão editar quaisquer configurações dos fluxos de trabalho",
|
||||
"locked_by_admin": "Bloqueado pelo administrador",
|
||||
"app_not_connected": "Não associou uma conta {{appName}}.",
|
||||
"connect_now": "Associar agora",
|
||||
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "Membrii vor putea vedea aplicațiile active, însă nu le vor putea modifica deloc setările",
|
||||
"locked_webhooks_description": "Membrii vor putea vedea webhook-urile active, însă nu le vor putea modifica deloc setările",
|
||||
"locked_workflows_description": "Membrii vor putea vedea fluxurile de lucru active, însă nu le vor putea modifica deloc setările",
|
||||
"locked_by_admin": "Blocat de administrator",
|
||||
"app_not_connected": "Nu ați conectat un cont {{appName}}.",
|
||||
"connect_now": "Conectați-l acum",
|
||||
"managed_event_dialog_confirm_button_one": "Înlocuiți și anunțați {{count}} membru",
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "Участники смогут видеть активные приложения, но не смогут редактировать их настройки",
|
||||
"locked_webhooks_description": "Участники смогут видеть активные вебхуки, но не смогут редактировать их настройки",
|
||||
"locked_workflows_description": "Участники смогут видеть активные рабочие процессы, но не смогут редактировать их настройки",
|
||||
"locked_by_admin": "Заблокировано администратором",
|
||||
"app_not_connected": "Вы не подключили аккаунт {{appName}}.",
|
||||
"connect_now": "Подключить сейчас",
|
||||
"managed_event_dialog_confirm_button_one": "Заменить и уведомить {{count}} участника",
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "Članovi će moći da vide aktivne aplikacije, ali neće moći da uređuju bilo kakva podešavanja aplikacije",
|
||||
"locked_webhooks_description": "Članovi će moći da vide aktivne webhook-ove, ali neće moći da uređuju bilo kakva podešavanja webhook-ova",
|
||||
"locked_workflows_description": "Članovi će moći da vide aktivne radne tokove, ali neće moći da uređuju bilo kakva podešavanja radnih tokova",
|
||||
"locked_by_admin": "Zaključao administrator",
|
||||
"app_not_connected": "Niste povezali {{appName}} nalog.",
|
||||
"connect_now": "Povežite odmah",
|
||||
"managed_event_dialog_confirm_button_one": "Zameni i obavesti {{count}} člana",
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "Medlemmarna kommer att kunna se de aktiva apparna, men de kommer inte att kunna ändra appinställningarna",
|
||||
"locked_webhooks_description": "Medlemmarna kommer att kunna se aktiva webhooks men inte kunna redigera inställningar för webhooks",
|
||||
"locked_workflows_description": "Medlemmarna kommer att kunna se de aktiva arbetsflödena, men de kommer inte att kunna ändra några arbetsflödesinställningar",
|
||||
"locked_by_admin": "Låst av administratör",
|
||||
"app_not_connected": "Du har inte anslutit ett {{appName}}-konto.",
|
||||
"connect_now": "Anslut nu",
|
||||
"managed_event_dialog_confirm_button_one": "Ersätt och meddela {{count}} medlem",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"identity_provider": "அடையாள வழங்குபவர்",
|
||||
"trial_days_left": "உங்கள் மேம்படுத்தப்பட்ட பயன்பாட்டு சோதனையில் $t(day, {\"count\": {{days}} }) மீதமுள்ளது",
|
||||
"day_one": "{{count}} நாள்",
|
||||
"day_other": "{{count}} நாட்கள்",
|
||||
@@ -7,6 +8,7 @@
|
||||
"upgrade_now": "மேம்படுத்தவும்",
|
||||
"accept_invitation": "அழைப்பை ஏற்கவும்",
|
||||
"calcom_explained": "{{appName}} அனைவருக்கும் திட்டமிடல் உள்கட்டமைப்பை வழங்குகிறது.",
|
||||
"calcom_explained_new_user": "உங்கள் {{appName}} கணக்கை அமைக்கவும்! உங்கள் திட்டமிடல் சிக்கல்கள் அனைத்தையும் தீர்க்க சில படிகள் மட்டுமே உள்ளன.",
|
||||
"have_any_questions": "கேள்விகள் உள்ளதா? நாங்கள் உதவ தயாராக இருக்கிறோம்.",
|
||||
"reset_password_subject": "{{appName}}: கடவுச்சொல் மீட்டமைக்கும் வழிமுறைகள்",
|
||||
"event_declined_subject": "{{title}} {{date}} அன்று நிராகரிக்கப்பட்டது",
|
||||
@@ -28,8 +30,58 @@
|
||||
"your_event_has_been_scheduled": "உங்கள் நிகழ்வு திட்டமிடப்பட்டது",
|
||||
"your_event_has_been_scheduled_recurring": "உங்கள் தொடர் நிகழ்வு திட்டமிடப்பட்டுள்ளது",
|
||||
"accept_our_license": ".env மாறியை மாற்றுவதன் மூலம் எங்கள் உரிமத்தை ஏற்கவும்\n<1>NEXT_PUBLIC_LICENSE_CONSENT</1> to '{{agree}}'.",
|
||||
"error_message": "பிழை செய்தி: '{{errorMessage}}'",
|
||||
"refund_failed_subject": "பணத்தைத் திரும்பப் பெற முடியவில்லை: {{name}} - {{date}} - {{eventType}}",
|
||||
"refund_failed": "{{date}} அன்று {{userName}} உடன் நடந்த {{eventType}} நிகழ்விற்கான பணத்தைத் திரும்பி அனுப்ப இயலவில்லை.",
|
||||
"check_with_provider_and_user": "உங்கள் கட்டண வழங்குநர் மற்றும் {{user}} உடன் இனைந்து இதை எவ்வாறு கையாள்வது என்பதைச் சரிபார்க்கவும்.",
|
||||
"a_refund_failed": "பணத்தைத் திரும்பப் பெற முடியவில்லை",
|
||||
"awaiting_payment_subject": "பணம் செலுத்த காத்திருக்கிறது: {{date}} அன்று {{title}}",
|
||||
"meeting_awaiting_payment": "உங்கள் மீட்டிங் கட்டணம் செலுத்த காத்திருக்கிறது",
|
||||
"help": "உதவி",
|
||||
"price": "விலை",
|
||||
"paid": "பணம் செலுத்தப்பட்டது",
|
||||
"refunded": "பணம் திருப்பிக் கொடுக்கப்பட்டது",
|
||||
"payment": "பணம் செலுத்துதல்",
|
||||
"missing_card_fields": "கார்டு புலங்கள் இல்லை",
|
||||
"pay_now": "இப்போது பணம் செலுத்த",
|
||||
"codebase_has_to_stay_opensource": "கோட்பேஸ் மாற்றப்பட்டாலும் இல்லாவிட்டாலும் திறந்த மூலமாக இருக்க வேண்டும்",
|
||||
"cannot_repackage_codebase": "நீங்கள் கோட்பேஸை மீண்டும் பேக்கேஜ் செய்யவோ விற்கவோ முடியாது",
|
||||
"acquire_license": "மின்னஞ்சல்மூலம் இந்த விதிமுறைகளை அகற்ற வணிக உரிமத்தைப் பெறவும்",
|
||||
"terms_summary": "விதிமுறைகளின் சுருக்கம்",
|
||||
"open_env": ".env ஐத் திறந்து எங்கள் உரிமத்தை ஏற்கவும்",
|
||||
"env_changed": "நான் என் .env ஐ மாற்றிவிட்டேன்",
|
||||
"accept_license": "உரிமத்தை ஏற்கவும்",
|
||||
"still_waiting_for_approval": "ஒரு நிகழ்வு இன்னும் ஒப்புதலுக்காகக் காத்திருக்கிறது",
|
||||
"event_is_still_waiting": "நிகழ்வு கோரிக்கை இன்னும் காத்திருக்கிறது: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"no_more_results": "மேலும் முடிவுகள் இல்லை",
|
||||
"no_results": "முடிவுகள் இல்லை",
|
||||
"load_more_results": "மேலும் முடிவுகளை ஏற்றவும்",
|
||||
"integration_meeting_id": "{{integrationName}} சந்திப்பு ஐடி: {{meetingId}}",
|
||||
"confirmed_event_type_subject": "உறுதிப்படுத்தப்பட்டது: {{date}} அன்று {{name}} உடன் {{eventType}}",
|
||||
"new_event_request": "புதிய நிகழ்வு கோரிக்கை: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"confirm_or_reject_request": "கோரிக்கையை உறுதிப்படுத்தவும் அல்லது நிராகரிக்கவும்",
|
||||
"check_bookings_page_to_confirm_or_reject": "முன்பதிவை உறுதிப்படுத்த அல்லது நிராகரிக்க உங்கள் முன்பதிவு பக்கத்தைப் பார்க்கவும்.",
|
||||
"event_awaiting_approval": "உங்கள் ஒப்புதலுக்காக ஒரு நிகழ்வு காத்திருக்கிறது",
|
||||
"event_awaiting_approval_recurring": "தொடர்ச்சியான நிகழ்வு உங்கள் ஒப்புதலுக்காகக் காத்திருக்கிறது",
|
||||
"someone_requested_an_event": "உங்கள் காலெண்டரில் ஒரு நிகழ்வைத் திட்டமிடுமாறு ஒருவர் கோரியுள்ளார்.",
|
||||
"someone_requested_password_reset": "உங்கள் கடவுச்சொல்லை மாற்ற யாரோ ஒரு இணைப்பைக் கோரியுள்ளனர்.",
|
||||
"password_reset_instructions": "நீங்கள் இதைக் கோரவில்லை எனில், இந்த மின்னஞ்சலைப் பாதுகாப்பாகப் புறக்கணிக்கலாம், மேலும் உங்கள் கடவுச்சொல் மாற்றப்படாது.",
|
||||
"event_awaiting_approval_subject": "ஒப்புதலுக்காகக் காத்திருக்கிறது: {{title}} {{date}} இல்",
|
||||
"event_still_awaiting_approval": "உங்கள் ஒப்புதலுக்காக ஒரு நிகழ்வு இன்னும் காத்திருக்கிறது",
|
||||
"booking_submitted_subject": "முன்பதிவு சமர்ப்பிக்கப்பட்டது: {{title}} {{date}} அன்று",
|
||||
"download_your_recording": "உங்கள் பதிவைப் பதிவிறக்கவும்",
|
||||
"your_meeting_has_been_booked": "உங்கள் சந்திப்பு பதிவு செய்யப்பட்டுள்ளது",
|
||||
"event_type_has_been_rescheduled_on_time_date": "உங்கள் {{title}}, {{date}} க்கு மாற்றியமைக்கப்பட்டுள்ளது.",
|
||||
"event_has_been_rescheduled": "புதுப்பிக்கப்பட்டது - உங்கள் நிகழ்வு மீண்டும் திட்டமிடப்பட்டது",
|
||||
"request_reschedule_title_attendee": "உங்கள் முன்பதிவை மாற்றியமைக்க கோரிக்கை",
|
||||
"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}} உடன் புதிய நேரத்தை முன்பதிவு செய்யவும்",
|
||||
"reschedule_reason": "மறு அட்டவணைக்கான காரணம்",
|
||||
"hi_user_name": "வணக்கம் {{name}}",
|
||||
"turn_on": "இயக்கவும்",
|
||||
"email_no_user_invite_heading": "நீங்கள் {{appName}} குழுவில் சேர அழைக்கப்பட்டுள்ளீர்கள்",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}} உங்களை {{appName}} குழுவில் சேர அழைத்துள்ளார். {{appName}} என்பது ஈமெயில் இல்லாமலேயே உங்களையும் உங்கள் குழுவையும் கூட்டங்களைத் திட்டமிடுவதற்கு உதவும் திட்டமிடல் பயன்பாடு.",
|
||||
"email_no_user_invite_steps_intro": "சில சிறிய முயர்ச்சித்தல் மூலம் நாங்கள் உங்களை அழைத்துச் செல்வோம், எந்த நேரத்திலும் உங்கள் குழுவுடன் மன அழுத்தமில்லாத திட்டமிடலை அனுபவிப்பீர்கள்.",
|
||||
@@ -114,6 +166,7 @@
|
||||
"select_all": "அனைத்தையும் தேர்வுசெய்",
|
||||
"default_conferencing_bulk_title": "தற்போதுள்ள நிகழ்வு வகைகளை மொத்தமாகப் புதுப்பிக்கவும்",
|
||||
"default_conferencing_bulk_description": "தேர்ந்தெடுக்கப்பட்ட நிகழ்வு வகைகளுக்கான இருப்பிடங்களைப் புதுப்பிக்கவும்",
|
||||
"locked_by_admin": "குழு நிர்வாகியால் பூட்டப்பட்டது",
|
||||
"looking_for_more_analytics": "மேலும் பகுப்பாய்வுகளைத் தேடுகிறீர்களா?",
|
||||
"add_filter": "வடிகட்டிச் சேர்க்கவும்",
|
||||
"select_user": "பயனரைத் தேர்ந்தெடுக்கவும்",
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "Üyeler aktif uygulamaları görebilir, ancak hiçbir uygulama ayarını düzenleyemez",
|
||||
"locked_webhooks_description": "Üyeler aktif web kancalarını görebilir, ancak hiçbir web kancası ayarını düzenleyemez",
|
||||
"locked_workflows_description": "Üyeler aktif iş akışlarını görebilir, ancak hiçbir iş akışı ayarını düzenleyemez",
|
||||
"locked_by_admin": "Yönetici tarafından kilitlendi",
|
||||
"app_not_connected": "Bir {{appName}} hesabı bağlamadınız.",
|
||||
"connect_now": "Hemen bağlan",
|
||||
"managed_event_dialog_confirm_button_one": "{{count}} üyeyi değiştirin ve bilgilendirin",
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "Учасники зможуть бачити активні додатки, але не зможуть редагувати налаштування додатка",
|
||||
"locked_webhooks_description": "Учасники бачитимуть активні вебгуки, але не зможуть змінювати їх налаштування",
|
||||
"locked_workflows_description": "Учасники бачитимуть активні робочі процеси, але не зможуть змінювати їх налаштування",
|
||||
"locked_by_admin": "Заблоковано адміністратором",
|
||||
"app_not_connected": "Ви не під’єднали обліковий запис {{appName}}.",
|
||||
"connect_now": "Під’єднати",
|
||||
"managed_event_dialog_confirm_button_one": "Замінити й повідомити {{count}} учасника",
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "Các thành viên có thể thấy được những ứng dụng đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của ứng dụng",
|
||||
"locked_webhooks_description": "Các thành viên có thể thấy được những webhook đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của webhook",
|
||||
"locked_workflows_description": "Các thành viên có thể thấy được những tiến độ công việc đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của tiến độ công việc",
|
||||
"locked_by_admin": "Bị khoá bởi quản trị viên",
|
||||
"app_not_connected": "Bạn không có tài khoản {{appName}} đã được kết nối.",
|
||||
"connect_now": "Kết nối ngay",
|
||||
"managed_event_dialog_confirm_button_one": "Thay thế & thông báo cho {{count}} thành viên",
|
||||
|
||||
@@ -1191,6 +1191,7 @@
|
||||
"create_workflow": "创建工作流程",
|
||||
"do_this": "执行此操作",
|
||||
"turn_off": "关闭",
|
||||
"turn_on": "打开",
|
||||
"settings_updated_successfully": "设置已成功更新",
|
||||
"error_updating_settings": "更新设置时出错",
|
||||
"personal_cal_url": "我的个人 {{appName}} URL",
|
||||
@@ -1726,7 +1727,7 @@
|
||||
"locked_apps_description": "成员将能够查看活动的应用,但不能编辑任何应用设置",
|
||||
"locked_webhooks_description": "成员将能够查看活动的 Webhook,但不能编辑任何 Webhook 设置",
|
||||
"locked_workflows_description": "成员将能够查看活动的工作流程,但不能编辑任何工作流程设置",
|
||||
"locked_by_admin": "被管理员锁定",
|
||||
"locked_by_admin": "被团队管理员锁定",
|
||||
"app_not_connected": "您尚未连接 {{appName}} 账户。",
|
||||
"connect_now": "立即连接",
|
||||
"managed_event_dialog_confirm_button_one": "替换并通知 {{count}} 个成员",
|
||||
|
||||
@@ -1724,7 +1724,6 @@
|
||||
"locked_apps_description": "成員將能查看啟用的應用程式,但無法編輯任何應用程式設定",
|
||||
"locked_webhooks_description": "成員將能查看啟用的 Webhook,但無法編輯任何 Webhook 設定",
|
||||
"locked_workflows_description": "成員將能查看啟用的工作流程,但無法編輯任何工作流程設定",
|
||||
"locked_by_admin": "已由管理員鎖定",
|
||||
"app_not_connected": "您尚未連結 {{appName}} 帳號。",
|
||||
"connect_now": "立即連結",
|
||||
"managed_event_dialog_confirm_button_one": "取代並通知 {{count}} 位成員",
|
||||
|
||||
@@ -151,10 +151,6 @@ const TestData = {
|
||||
},
|
||||
};
|
||||
|
||||
const ctx = {
|
||||
prisma: prismaMock,
|
||||
};
|
||||
|
||||
type App = {
|
||||
slug: string;
|
||||
dirName: string;
|
||||
@@ -186,6 +182,7 @@ type InputEventType = {
|
||||
id: number;
|
||||
title?: string;
|
||||
length?: number;
|
||||
offsetStart?: number;
|
||||
slotInterval?: number;
|
||||
minimumBookingNotice?: number;
|
||||
users?: { id: number }[];
|
||||
@@ -711,6 +708,72 @@ describe("getSchedule", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("Start times are offset (offsetStart)", async () => {
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
|
||||
const scenarioData = {
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
length: 25,
|
||||
offsetStart: 5,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
...TestData.users.example,
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
},
|
||||
],
|
||||
hosts: [],
|
||||
apps: [TestData.apps.googleCalendar],
|
||||
};
|
||||
|
||||
createBookingScenario(scenarioData);
|
||||
|
||||
const schedule = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
});
|
||||
|
||||
expect(schedule).toHaveTimeSlots(
|
||||
[
|
||||
`04:05:00.000Z`,
|
||||
`04:35:00.000Z`,
|
||||
`05:05:00.000Z`,
|
||||
`05:35:00.000Z`,
|
||||
`06:05:00.000Z`,
|
||||
`06:35:00.000Z`,
|
||||
`07:05:00.000Z`,
|
||||
`07:35:00.000Z`,
|
||||
`08:05:00.000Z`,
|
||||
`08:35:00.000Z`,
|
||||
`09:05:00.000Z`,
|
||||
`09:35:00.000Z`,
|
||||
`10:05:00.000Z`,
|
||||
`10:35:00.000Z`,
|
||||
`11:05:00.000Z`,
|
||||
`11:35:00.000Z`,
|
||||
`12:05:00.000Z`,
|
||||
],
|
||||
{
|
||||
dateString: plus2DateString,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("Check for Date overrides", async () => {
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
@@ -1132,6 +1195,7 @@ function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) {
|
||||
seatsPerTimeSlot: null,
|
||||
metadata: {},
|
||||
minimumBookingNotice: 0,
|
||||
offsetStart: 0,
|
||||
};
|
||||
const foundEvents: Record<number, boolean> = {};
|
||||
const eventTypesWithUsers = eventTypes.map((eventType) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Prisma } from "@prisma/client";
|
||||
|
||||
import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes";
|
||||
import { buildEventType } from "@calcom/lib/test/builder";
|
||||
import type { CompleteEventType } from "@calcom/prisma/zod";
|
||||
import type { CompleteEventType, CompleteWorkflowsOnEventTypes } from "@calcom/prisma/zod";
|
||||
|
||||
import { prismaMock } from "../../../../tests/config/singleton";
|
||||
|
||||
@@ -291,4 +291,85 @@ describe("handleChildrenEventTypes", () => {
|
||||
expect(result.deletedExistentEventTypes).toEqual([123]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Workflows", () => {
|
||||
it("Links workflows to new and existing assigned members", async () => {
|
||||
const { schedulingType, id, teamId, locations, timeZone, parentId, userId, ...evType } =
|
||||
mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
workflows: [
|
||||
{
|
||||
workflowId: 11,
|
||||
} as CompleteWorkflowsOnEventTypes,
|
||||
],
|
||||
});
|
||||
prismaMock.$transaction.mockResolvedValue([{ id: 2 }]);
|
||||
await updateChildrenEventTypes({
|
||||
eventTypeId: 1,
|
||||
oldEventType: { children: [{ userId: 4 }], team: { name: "" } },
|
||||
children: [
|
||||
{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: [] } },
|
||||
{ hidden: false, owner: { id: 5, name: "", email: "", eventTypeSlugs: [] } },
|
||||
],
|
||||
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
|
||||
currentUserId: 1,
|
||||
hashedLink: undefined,
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
});
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
bookingLimits: undefined,
|
||||
durationLimits: undefined,
|
||||
recurringEvent: undefined,
|
||||
hashedLink: undefined,
|
||||
locations: [],
|
||||
parentId: 1,
|
||||
userId: 5,
|
||||
users: {
|
||||
connect: [
|
||||
{
|
||||
id: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
workflows: {
|
||||
create: [{ workflowId: 11 }],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
bookingLimits: undefined,
|
||||
durationLimits: undefined,
|
||||
recurringEvent: undefined,
|
||||
hashedLink: undefined,
|
||||
workflows: undefined,
|
||||
scheduleId: undefined,
|
||||
},
|
||||
where: {
|
||||
userId_parentId: {
|
||||
userId: 4,
|
||||
parentId: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(prismaMock.workflowsOnEventTypes.upsert).toHaveBeenCalledWith({
|
||||
create: {
|
||||
eventTypeId: 2,
|
||||
workflowId: 11,
|
||||
},
|
||||
update: {},
|
||||
where: {
|
||||
workflowId_eventTypeId: {
|
||||
eventTypeId: 2,
|
||||
workflowId: 11,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ describe("Tests the slot logic", () => {
|
||||
},
|
||||
],
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
organizerTimeZone: "America/Toronto",
|
||||
})
|
||||
).toHaveLength(24);
|
||||
@@ -46,6 +47,7 @@ describe("Tests the slot logic", () => {
|
||||
},
|
||||
],
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
organizerTimeZone: "America/Toronto",
|
||||
})
|
||||
).toHaveLength(12);
|
||||
@@ -66,6 +68,7 @@ describe("Tests the slot logic", () => {
|
||||
},
|
||||
],
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
organizerTimeZone: "America/Toronto",
|
||||
})
|
||||
).toHaveLength(0);
|
||||
@@ -87,6 +90,7 @@ describe("Tests the slot logic", () => {
|
||||
minimumBookingNotice: 0,
|
||||
workingHours,
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
organizerTimeZone: "America/Toronto",
|
||||
})
|
||||
).toHaveLength(0);
|
||||
@@ -108,6 +112,7 @@ describe("Tests the slot logic", () => {
|
||||
},
|
||||
],
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
organizerTimeZone: "America/Toronto",
|
||||
})
|
||||
).toHaveLength(11);
|
||||
@@ -129,8 +134,30 @@ describe("Tests the slot logic", () => {
|
||||
},
|
||||
],
|
||||
eventLength: 20,
|
||||
offsetStart: 0,
|
||||
organizerTimeZone: "America/Toronto",
|
||||
})
|
||||
).toHaveLength(71);
|
||||
});
|
||||
|
||||
it("can fit 48 25 minute slots with a 5 minute offset for an empty day", async () => {
|
||||
expect(
|
||||
getSlots({
|
||||
inviteeDate: dayjs.utc().add(1, "day"),
|
||||
frequency: 25,
|
||||
minimumBookingNotice: 0,
|
||||
workingHours: [
|
||||
{
|
||||
userId: 1,
|
||||
days: Array.from(Array(7).keys()),
|
||||
startTime: MINUTES_DAY_START,
|
||||
endTime: MINUTES_DAY_END,
|
||||
},
|
||||
],
|
||||
eventLength: 25,
|
||||
offsetStart: 5,
|
||||
organizerTimeZone: "America/Toronto",
|
||||
})
|
||||
).toHaveLength(48);
|
||||
});
|
||||
});
|
||||
|
||||
+7
-2
@@ -23,8 +23,12 @@ for module in "$@"; do
|
||||
git submodule add --force $project "apps/$module"
|
||||
# Set the default branch to main
|
||||
git config -f .gitmodules --add "submodule.apps/$module.branch" main
|
||||
# Adding the subdmoule ignores the `.gitignore` so a reset is needed
|
||||
git reset
|
||||
|
||||
# Update to the latest from main in that submodule
|
||||
cd apps/$module && git pull origin main && cd ../..
|
||||
|
||||
# We forcefully added the subdmoule which was in .gitignore, so unstage it.
|
||||
git restore --staged apps/$module
|
||||
else
|
||||
# If the module is the API, display a link to request access
|
||||
if [ "$module" = "api" ]; then
|
||||
@@ -35,3 +39,4 @@ for module in "$@"; do
|
||||
fi
|
||||
fi
|
||||
done
|
||||
git restore --staged .gitmodules
|
||||
|
||||
+3
-1
@@ -17,6 +17,7 @@
|
||||
"app-store": "yarn app-store-cli cli",
|
||||
"create-app": "yarn app-store create",
|
||||
"edit-app": "yarn app-store edit",
|
||||
"publish-embed": "yarn workspaces foreach --from=\"@calcom/embed*\" npm publish --access=public",
|
||||
"delete-app": "yarn app-store delete",
|
||||
"create-app-template": "yarn app-store create-template",
|
||||
"edit-app-template": "yarn app-store edit-template",
|
||||
@@ -74,6 +75,7 @@
|
||||
"web": "yarn workspace @calcom/web"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.26.1",
|
||||
"@deploysentinel/playwright": "^0.3.3",
|
||||
"@playwright/test": "^1.31.2",
|
||||
"@snaplet/copycat": "^0.3.0",
|
||||
@@ -95,7 +97,7 @@
|
||||
"dependencies": {
|
||||
"city-timezones": "^1.2.1",
|
||||
"eslint": "^8.34.0",
|
||||
"lucide-react": "^0.130.0",
|
||||
"lucide-react": "^0.171.0",
|
||||
"turbo": "^1.8.3"
|
||||
},
|
||||
"resolutions": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/app-store-cli",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"version": "0.0.0",
|
||||
"bin": "dist/cli.js",
|
||||
|
||||
@@ -4,9 +4,9 @@ import SelectInput from "ink-select-input";
|
||||
import TextInput from "ink-text-input";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { AppMeta } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import { getSlugFromAppName, BaseAppFork, Seed, generateAppFiles, getAppDirPath } from "../core";
|
||||
import { getSlugFromAppName, BaseAppFork, generateAppFiles, getAppDirPath } from "../core";
|
||||
import { getApp } from "../utils/getApp";
|
||||
import Templates from "../utils/templates";
|
||||
import Label from "./Label";
|
||||
@@ -148,8 +148,6 @@ export const AppForm = ({
|
||||
oldSlug: givenSlug,
|
||||
});
|
||||
|
||||
await Seed.update({ slug, category: category, oldSlug: givenSlug, isTemplate });
|
||||
|
||||
await generateAppFiles();
|
||||
|
||||
// FIXME: Even after CLI showing this message, it is stuck doing work before exiting
|
||||
@@ -242,6 +240,10 @@ export const AppForm = ({
|
||||
<Text color="green">Publisher Email: </Text>
|
||||
<Text>{email}</Text>
|
||||
</Box>
|
||||
<Text bold>
|
||||
Next Step: Enable the app from http://localhost:3000/settings/admin/apps as admin user (Email:
|
||||
admin@example.com, Pass: ADMINadmin2022!)
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from "react";
|
||||
|
||||
import { ImportantText } from "../components/ImportantText";
|
||||
import { Message } from "../components/Message";
|
||||
import { BaseAppFork, Seed, generateAppFiles } from "../core";
|
||||
import { BaseAppFork, generateAppFiles } from "../core";
|
||||
import { getApp } from "../utils/getApp";
|
||||
|
||||
export default function DeleteForm({ slug, action }: { slug: string; action: "delete" | "delete-template" }) {
|
||||
@@ -28,7 +28,6 @@ export default function DeleteForm({ slug, action }: { slug: string; action: "de
|
||||
if (state === "DELETION_CONFIRMATION_SUCCESSFUL") {
|
||||
(async () => {
|
||||
await BaseAppFork.delete({ slug, isTemplate });
|
||||
Seed.revert({ slug });
|
||||
await generateAppFiles();
|
||||
// successMsg({ text: `App with slug ${slug} has been deleted`, done: true });
|
||||
setState("DELETION_COMPLETED");
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import type seedAppStoreConfig from "@calcom/prisma/seed-app-store.config.json";
|
||||
|
||||
import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants";
|
||||
import execSync from "./utils/execSync";
|
||||
|
||||
@@ -27,10 +25,6 @@ export function getAppDirPath(slug: string, isTemplate: boolean) {
|
||||
return path.join(TEMPLATES_PATH, `${slug}`);
|
||||
}
|
||||
|
||||
function absolutePath(appRelativePath: string) {
|
||||
return path.join(APP_STORE_PATH, appRelativePath);
|
||||
}
|
||||
|
||||
const updatePackageJson = ({
|
||||
slug,
|
||||
appDescription,
|
||||
@@ -138,58 +132,6 @@ export const BaseAppFork = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Seed = {
|
||||
seedConfigPath: absolutePath("../prisma/seed-app-store.config.json"),
|
||||
update: async function ({
|
||||
slug,
|
||||
category,
|
||||
oldSlug,
|
||||
isTemplate,
|
||||
}: {
|
||||
slug: string;
|
||||
category: string;
|
||||
oldSlug: string;
|
||||
isTemplate: boolean;
|
||||
}) {
|
||||
let configContent = "[]";
|
||||
try {
|
||||
if (fs.statSync(this.seedConfigPath)) {
|
||||
configContent = fs.readFileSync(this.seedConfigPath).toString();
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
let seedConfig: typeof seedAppStoreConfig = JSON.parse(configContent);
|
||||
seedConfig = seedConfig.filter((app) => app.slug !== oldSlug);
|
||||
|
||||
if (!seedConfig.find((app) => app.slug === slug)) {
|
||||
seedConfig.push({
|
||||
dirName: slug,
|
||||
categories: [category],
|
||||
slug: slug,
|
||||
type: `${slug}_${category}`,
|
||||
isTemplate: isTemplate,
|
||||
});
|
||||
}
|
||||
|
||||
// Add the message as a property to first item so that it stays always at the top
|
||||
seedConfig[0]["/*"] =
|
||||
"This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually";
|
||||
|
||||
// Add the message as a property to first item so that it stays always at the top
|
||||
seedConfig[0]["/*"] =
|
||||
"This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually";
|
||||
|
||||
fs.writeFileSync(this.seedConfigPath, JSON.stringify(seedConfig, null, 2));
|
||||
await execSync(`cd ${workspaceDir}/packages/prisma && yarn seed-app-store seed-templates`);
|
||||
},
|
||||
revert: async function ({ slug }: { slug: string }) {
|
||||
let seedConfig: typeof seedAppStoreConfig = JSON.parse(fs.readFileSync(this.seedConfigPath).toString());
|
||||
seedConfig = seedConfig.filter((app) => app.slug !== slug);
|
||||
fs.writeFileSync(this.seedConfigPath, JSON.stringify(seedConfig, null, 2));
|
||||
await execSync(`yarn workspace @calcom/prisma delete-app ${slug}`);
|
||||
},
|
||||
};
|
||||
|
||||
export const generateAppFiles = async () => {
|
||||
await execSync(`yarn ts-node --transpile-only src/build.ts`);
|
||||
};
|
||||
|
||||
@@ -5,8 +5,22 @@ import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
import type { AppFrontendPayload as App } from "@calcom/types/App";
|
||||
import type { CredentialFrontendPayload as Credential } from "@calcom/types/Credential";
|
||||
|
||||
export async function getAppWithMetadata(app: { dirName: string }) {
|
||||
const appMetadata: App | null = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata] as App;
|
||||
/**
|
||||
* Get App metdata either using dirName or slug
|
||||
*/
|
||||
export async function getAppWithMetadata(app: { dirName: string } | { slug: string }) {
|
||||
let appMetadata: App | null;
|
||||
|
||||
if ("dirName" in app) {
|
||||
appMetadata = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata] as App;
|
||||
} else {
|
||||
const foundEntry = Object.entries(appStoreMetadata).find(([, meta]) => {
|
||||
return meta.slug === app.slug;
|
||||
});
|
||||
if (!foundEntry) return null;
|
||||
appMetadata = foundEntry[1] as App;
|
||||
}
|
||||
|
||||
if (!appMetadata) return null;
|
||||
// Let's not leak api keys to the front end
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
@@ -47,6 +47,7 @@ export default function OmniInstallAppButton({
|
||||
<InstallAppButton
|
||||
type={app.type}
|
||||
isProOnly={app.isProOnly}
|
||||
teamsPlanRequired={app.teamsPlanRequired}
|
||||
wrapperClassName={classNames("[@media(max-width:260px)]:w-full", className)}
|
||||
render={({ useDefaultComponent, ...props }) => {
|
||||
if (useDefaultComponent) {
|
||||
|
||||
@@ -12,7 +12,14 @@ export const getCalendar = async (credential: CredentialPayload | null): Promise
|
||||
if (calendarType?.endsWith("_other_calendar")) {
|
||||
calendarType = calendarType.split("_other_calendar")[0];
|
||||
}
|
||||
const calendarApp = await appStore[calendarType.split("_").join("") as keyof typeof appStore]();
|
||||
const calendarAppImportFn = appStore[calendarType.split("_").join("") as keyof typeof appStore];
|
||||
|
||||
if (!calendarAppImportFn) {
|
||||
log.warn(`calendar of type ${calendarType} is not implemented`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const calendarApp = await calendarAppImportFn();
|
||||
if (!(calendarApp && "lib" in calendarApp && "CalendarService" in calendarApp.lib)) {
|
||||
log.warn(`calendar of type ${calendarType} is not implemented`);
|
||||
return null;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user