Compare commits

...
27 Commits
Author SHA1 Message Date
Keith Williams 0b719ac070 chore: v4.4.2 (#16278)
release.yml / release (push) Canceled after 0s
2024-08-20 22:54:22 +09:00
Anik Dhabal BabuandSomay Chauhan baa9045af7 feat: ability to add guests via app.cal.com/bookings (#14740)
* feat: ability to add guests via app.cal.com/bookings

* fix: some update

* fix: minor issue

* fix: final update

* update

* update

* add requested changes

* fix type error

* small update

* final update

* fix type error

* fix location

* update calender event

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2024-08-20 09:26:19 +00:00
b004587262 fix: textarea ux in Editor.tsx #16225 (#16231)
* fix: textarea ux in Editor.tsx #16225

* fix: textarea ux in Editor.tsx #16225

* fix: textarea ux in Editor.tsx #16225

* fix: textarea ux in Editor.tsx #16225

* fix: textarea ux in Editor.tsx #16225

* fix and update

* remove space

---------

Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-08-20 08:39:33 +00:00
Sy Sagar 8a7beff339 style(Booker): overflow on timezone Select (#16242) 2024-08-20 08:26:53 +00:00
Shreya AgarwalandAmit Sharma 9001eb7795 fix: fix app config same key values (#16215)
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-08-20 07:33:24 +00:00
Hichem Fantar 50c4578c2e feat: Add transition to hover background in multiple components (#16216)
* chore: Add transition to hover background in multiple components

* chore: Add transition to hover background in multiple components
2024-08-20 07:23:09 +00:00
4ff2ce90f9 fix: whatsapp message body sometimes editable (Issue #16067) (#16185)
* Fixing issue #16067

* Creating reusable Editor component for Editor

* fix editor issue

* remove log

* fix: unit test

* fix: unit test

* add unit test for the plugin and changes

---------

Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-08-20 16:19:24 +09:00
86f8c6c5fa fix: Close.com can't be installed (#16196)
* Fix for #16183 , [CAL-3969]

* Update index.tsx

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2024-08-20 16:18:20 +09:00
Vijay 0611bbd50e fix: #16079 hide settings/Admin API if not org and update link to sales (#16198)
* hide admin_api if not org and update link to sales

* update for org members

* updated back to orgAdmin check
2024-08-20 16:17:24 +09:00
Omar López b946abab02 fix: only keep latest e2e test results (#16273) 2024-08-19 23:23:06 +00:00
Omar López a091c60618 fix: uncaught types errors in invite members team handler (#16109) 2024-08-19 20:27:37 +00:00
Omar López bba7fc1a30 hotfix: wrong branch name for reports (#16271) 2024-08-19 20:18:15 +00:00
zomars 864c46736b hotfix: wrong branch name for reports
Signed-off-by: zomars <zomars@me.com>
2024-08-19 12:59:44 -07:00
4416709ff6 chore(availabilty-page): Bulk update modal (#16267)
* chore(Availability): Bulk update modal

* fix type error

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
2024-08-19 21:34:16 +02:00
Vijay 591d42fc8f fix: #16150 update to fix seated event reschedule (#16163)
* update to fix seated event reschedule

* updated handleseats tests

* updated handleSeats timeslot

* updated test to expect error

* update to remove uid in where clause (isFirstSeat)
2024-08-19 22:03:35 +05:30
Syed Ali Shahbaz d73b84157a fix: v1 api payments endpoint fix (#16264)
* Update payment validation v1

* fix for [id]
2024-08-19 16:29:36 +00:00
680e748b7a feat: make the meeting duration time more visually toggleable (#15422)
* feat: make the meeting duration time more visually toggleable

* final_update

* Update dynamic-booking-pages.e2e.ts

* fix e2e

* fix: e2e test

* fixed

* update

* show selcted item at the time of mount

* fix type error

* Update Duration.tsx

* fix

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-08-19 14:25:02 +00:00
zerone0xandunknown 99b7aaa62d fix: Chinese traditional shows multiple durations of the event as a string (#16187)
* fix: Chinese traditional shows multiple durations of the event as a string

* revert

* make changes only in necessary files

---------

Co-authored-by: unknown <adhabal2002@gmail.com>
2024-08-19 19:34:30 +09:00
Omar López 3a01e6390f fix: type issues in attributes-edit-view (#16243) 2024-08-19 10:33:20 +00:00
dillonstreatorandunknown a6c25ab7d3 docs: update documentation references to include /docs base path (#16257)
* docs: update /doc references

* gix avilibilty docs link

---------

Co-authored-by: unknown <adhabal2002@gmail.com>
2024-08-19 10:21:50 +00:00
Rajiv Sahal df98274a8f update icons list (#16263) 2024-08-19 10:21:36 +00:00
Kaiwalya Koparkar b540a2ec45 update: Deploy to Elestio button updated (#16238) 2024-08-19 19:16:35 +09:00
sean-brydon 8e4459f2fe chore: missing ff attributes migration (#16229)
* add migration file

* add miration file
2024-08-19 10:09:25 +00:00
Carina WollendorferandCarinaWolli bc5a9f231f fix: POST v1/teams request body (#16262)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-08-19 08:28:56 +00:00
Carina WollendorferandCarinaWolli 88984f6175 fix: event name in attendee cancellation email (#16240)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-08-19 08:24:52 +00:00
fc592a17ac feat: Add 'Disable on Prefill' option for fields (#15715)
* feat:add prefilcheck check box in the field creation

* chore:formatted the li8 translations

* feat:add e2e test case for this feature

* refactor:reanmed the key from prefilledCheck to disableOnPrefill and moved the readOnly logic to formbuilderField

* chore:fixed the type check

* fix:if prefilled has error. do not disabble it

* chore:removed the e2e tests for prefill in e2e

* chore:revert back to old changes

* chore:remove unwanted code frome2e

* Support name field as well

* refactor:handled the feedback changes. still unit test cases pending

* feat: add unit tests for  disable field on prefill logic

* chore: resolved the type check and typos

* chore:renamed the files and type clean up

* Apply suggestions from code review

* chore:renaming test files and moving the disableonprefill hook to formbuilderFeild.

* refactor: add unit test for serach params and form values check and minor code formatting and file name changes

* fix: validating the input value partially at the intialising phase. so that form response values and  from ui values are same and making sure invalid data not sent to backend on submit.

* chore:fix the type check issue

* fix: field value valdiation at initilisation and add the  unit test cases for that change

* chore:add comments to the tests

* chore:resolve type check

* chore: moved the tests to the formBuilder

* Simplify codebase by ensuring select and multiselect changes the value when no option is found

* Relocate and refactor tests

* handle special cases

* Dont disable for dirty fields

* Update schema.prisma

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-08-18 17:34:59 +00:00
Udit Takkar a51524e673 fix: cal video bug fix and improvements (#16234) 2024-08-18 09:09:27 +00:00
140 changed files with 2289 additions and 239 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
## Mandatory Tasks (DO NOT REMOVE)
- [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [ ] I have added a Docs issue [here](https://github.com/calcom/docs/issues/new) if this PR makes changes that would require a [documentation change](https://docs.cal.com). If N/A, write N/A here and check the checkbox.
- [ ] I have added a Docs issue [here](https://github.com/calcom/docs/issues/new) if this PR makes changes that would require a [documentation change](https://docs.cal.com/docs). If N/A, write N/A here and check the checkbox.
- [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works.
## How should this be tested?
+21 -3
View File
@@ -10,7 +10,8 @@ jobs:
continue-on-error: true
env:
# Unique URL path for each workflow run attempt
HTML_REPORT_URL_PATH: reports/${{ github.ref_name }}/${{ github.run_id }}/${{ github.run_attempt }}
HTML_REPORT_URL_PATH: reports/${{ github.head_ref }}/${{ github.run_id }}/${{ github.run_attempt }}
BRANCH_REPORTS_DIR: reports/${{ github.head_ref }}
steps:
- name: Checkout GitHub Pages Branch
uses: actions/checkout@v4
@@ -21,6 +22,20 @@ jobs:
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Check for workflow reports
run: |
if [ -z "$(ls -A $BRANCH_REPORTS_DIR)" ]; then
echo "BRANCH_REPORTS_EXIST="false"" >> $GITHUB_ENV
else
echo "BRANCH_REPORTS_EXIST="true"" >> $GITHUB_ENV
fi
- name: Cleanup old HTML reports
if: ${{ env.BRANCH_REPORTS_EXIST == 'true' }}
timeout-minutes: 3
run: |
rm -rf $BRANCH_REPORTS_DIR
git add .
git commit -m "workflow: remove all reports for branch ${{ github.head_ref }}"
- name: Download zipped HTML report
uses: actions/download-artifact@v4
with:
@@ -68,11 +83,14 @@ jobs:
body: |
<!-- GENERATED-E2E-RESULTS -->
## E2E results are ready!
- [Attempt #${{ github.run_number }}.${{ github.run_attempt }} results](${{ steps.url.outputs.link }})
- [Workflow #${{ github.run_number }}.${{ github.run_attempt }} latest results](${{ steps.url.outputs.link }})
- name: Update comment
if: steps.fc.outputs.comment-id != ''
uses: peter-evans/create-or-update-comment@v3
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
edit-mode: replace
body: |
- [Attempt #${{ github.run_number }}.${{ github.run_attempt }} results](${{ steps.url.outputs.link }})
<!-- GENERATED-E2E-RESULTS -->
## E2E results are ready!
- [Workflow #${{ github.run_number }}.${{ github.run_attempt }} latest results](${{ steps.url.outputs.link }})
+1 -1
View File
@@ -380,7 +380,7 @@ Currently Vercel Pro Plan is required to be able to Deploy this application with
### Elestio
[![Deploy on Elestio](https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png)](https://elest.io/open-source/cal.com)
[![Deploy on Elestio](https://elest.io/images/logos/deploy-to-elestio-btn.png)](https://elest.io/open-source/cal.com)
<!-- ROADMAP -->
+10 -3
View File
@@ -1,5 +1,12 @@
import { _PaymentModel as Payment } from "@calcom/prisma/zod";
// FIXME: Payment seems a delicate endpoint, do we need to remove anything here?
export const schemaPaymentBodyParams = Payment.omit({ id: true });
export const schemaPaymentPublic = Payment.omit({ externalId: true });
export const schemaPaymentPublic = Payment.pick({
id: true,
amount: true,
success: true,
refunded: true,
fee: true,
paymentOption: true,
currency: true,
bookingId: true,
});
+1
View File
@@ -9,6 +9,7 @@ export const schemaTeamBaseBodyParams = Team.omit({ id: true, createdAt: true })
isOrganization: true,
isPlatform: true,
smsLockState: true,
smsLockReviewedByAdmin: true,
});
const schemaTeamRequiredParams = z.object({
@@ -27,7 +27,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
* tags:
* - availabilities
* externalDocs:
* url: https://docs.cal.com/availability
* url: https://docs.cal.com/docs/core-features/availability
* responses:
* 201:
* description: OK, availability removed successfully
@@ -28,7 +28,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
* tags:
* - availabilities
* externalDocs:
* url: https://docs.cal.com/availability
* url: https://docs.cal.com/docs/core-features/availability
* responses:
* 200:
* description: OK
@@ -63,7 +63,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
* tags:
* - availabilities
* externalDocs:
* url: https://docs.cal.com/availability
* url: https://docs.cal.com/docs/core-features/availability
* responses:
* 201:
* description: OK, availability edited successfully
@@ -62,7 +62,7 @@ import {
* tags:
* - availabilities
* externalDocs:
* url: https://docs.cal.com/availability
* url: https://docs.cal.com/docs/core-features/availability
* responses:
* 201:
* description: OK, availability created
+2 -2
View File
@@ -11,8 +11,8 @@ const swaggerHandler = withSwagger({
{ url: "https://api.cal.com/v1" },
],
externalDocs: {
url: "https://docs.cal.com",
description: "Find more info at our main docs: https://docs.cal.com/",
url: "https://docs.cal.com/docs",
description: "Find more info at our main docs: https://docs.cal.com/docs/",
},
info: {
title: `${pjson.name}: ${pjson.description}`,
@@ -28,7 +28,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
* tags:
* - event-types
* externalDocs:
* url: https://docs.cal.com/core-features/event-types
* url: https://docs.cal.com/docs/core-features/event-types
* responses:
* 201:
* description: OK, eventType removed successfully
@@ -34,7 +34,7 @@ import getCalLink from "../_utils/getCalLink";
* tags:
* - event-types
* externalDocs:
* url: https://docs.cal.com/core-features/event-types
* url: https://docs.cal.com/docs/core-features/event-types
* responses:
* 200:
* description: OK
@@ -194,7 +194,7 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
* tags:
* - event-types
* externalDocs:
* url: https://docs.cal.com/core-features/event-types
* url: https://docs.cal.com/docs/core-features/event-types
* responses:
* 201:
* description: OK, eventType edited successfully
+1 -1
View File
@@ -33,7 +33,7 @@ import getCalLink from "./_utils/getCalLink";
* tags:
* - event-types
* externalDocs:
* url: https://docs.cal.com/core-features/event-types
* url: https://docs.cal.com/docs/core-features/event-types
* responses:
* 200:
* description: OK
+1 -1
View File
@@ -255,7 +255,7 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
* tags:
* - event-types
* externalDocs:
* url: https://docs.cal.com/core-features/event-types
* url: https://docs.cal.com/docs/core-features/event-types
* responses:
* 201:
* description: OK, event type created
@@ -125,6 +125,7 @@ export async function patchHandler(req: NextApiRequest) {
metadata: NonNullable<typeof data.metadata> | undefined;
} = {
...data,
smsLockReviewedByAdmin: false,
metadata: data.metadata === null ? {} : data.metadata || undefined,
};
const team = await prisma.team.update({ where: { id: teamId }, data: cloneData });
@@ -32,7 +32,7 @@ const querySchema = z.object({
* tags:
* - event-types
* externalDocs:
* url: https://docs.cal.com/core-features/event-types
* url: https://docs.cal.com/docs/core-features/event-types
* responses:
* 200:
* description: OK
+1
View File
@@ -125,6 +125,7 @@ async function postHandler(req: NextApiRequest) {
metadata: NonNullable<typeof data.metadata> | undefined;
} = {
...data,
smsLockReviewedByAdmin: false,
metadata: data.metadata === null ? {} : data.metadata || undefined,
};
@@ -27,7 +27,7 @@ import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
* tags:
* - webhooks
* externalDocs:
* url: https://docs.cal.com/core-features/webhooks
* url: https://docs.cal.com/docs/core-features/webhooks
* responses:
* 201:
* description: OK, hook removed successfully
+1 -1
View File
@@ -28,7 +28,7 @@ import { schemaWebhookReadPublic } from "~/lib/validations/webhook";
* tags:
* - webhooks
* externalDocs:
* url: https://docs.cal.com/core-features/webhooks
* url: https://docs.cal.com/docs/core-features/webhooks
* responses:
* 200:
* description: OK
@@ -58,7 +58,7 @@ import { schemaWebhookEditBodyParams, schemaWebhookReadPublic } from "~/lib/vali
* tags:
* - webhooks
* externalDocs:
* url: https://docs.cal.com/core-features/webhooks
* url: https://docs.cal.com/docs/core-features/webhooks
* responses:
* 201:
* description: OK, webhook edited successfully
+1 -1
View File
@@ -24,7 +24,7 @@ import { schemaWebhookReadPublic } from "~/lib/validations/webhook";
* tags:
* - webhooks
* externalDocs:
* url: https://docs.cal.com/core-features/webhooks
* url: https://docs.cal.com/docs/core-features/webhooks
* responses:
* 200:
* description: OK
+1 -1
View File
@@ -56,7 +56,7 @@ import { schemaWebhookCreateBodyParams, schemaWebhookReadPublic } from "~/lib/va
* tags:
* - webhooks
* externalDocs:
* url: https://docs.cal.com/core-features/webhooks
* url: https://docs.cal.com/docs/core-features/webhooks
* responses:
* 201:
* description: OK, webhook created
@@ -177,6 +177,10 @@ export class BookingField_2024_04_15 {
@ValidateNested({ each: true })
@Type(() => Source)
sources?: Source[];
@IsOptional()
@IsBoolean()
disableOnPrefill?: boolean;
}
export class RecurringEvent_2024_04_15 {
@@ -87,6 +87,10 @@ class BookingField {
@Type(() => Source)
@IsOptional()
sources?: Source[];
@IsOptional()
@IsBoolean()
disableOnPrefill?: boolean;
}
class Organization {
@@ -29,7 +29,7 @@ const AccountSelector: FC<AccountSelectorProps> = ({
return (
<div
className={classNames(
"hover:bg-muted flex cursor-pointer flex-row items-center gap-2 p-1",
"hover:bg-muted flex cursor-pointer flex-row items-center gap-2 p-1 transition",
(alreadyInstalled || loading) && "cursor-not-allowed",
selected && loading && "bg-muted animate-pulse"
)}
@@ -94,7 +94,7 @@ const EventTypeCard: FC<EventTypeCardProps> = ({
return (
<div
data-testid={`select-event-type-${id}`}
className="hover:bg-muted min-h-20 box-border flex w-full cursor-pointer select-none items-center space-x-4 px-4 py-3"
className="hover:bg-muted min-h-20 box-border flex w-full cursor-pointer select-none items-center space-x-4 px-4 py-3 transition"
onClick={() => handleSelect()}>
<input
id={`${id}`}
@@ -43,6 +43,7 @@ import {
Tooltip,
} from "@calcom/ui";
import { AddGuestsDialog } from "@components/dialog/AddGuestsDialog";
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { ReassignDialog } from "@components/dialog/ReassignDialog";
@@ -189,6 +190,14 @@ function BookingListItem(booking: BookingItemProps) {
},
icon: "map-pin" as const,
},
{
id: "add_members",
label: t("additional_guests"),
onClick: () => {
setIsOpenAddGuestsDialog(true);
},
icon: "user-plus" as const,
},
];
if (booking.eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
@@ -256,6 +265,7 @@ function BookingListItem(booking: BookingItemProps) {
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
const [isOpenReassignDialog, setIsOpenReassignDialog] = useState(false);
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
const [isOpenAddGuestsDialog, setIsOpenAddGuestsDialog] = useState(false);
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
onSuccess: () => {
showToast(t("location_updated"), "success");
@@ -344,6 +354,11 @@ function BookingListItem(booking: BookingItemProps) {
setShowLocationModal={setIsOpenLocationDialog}
teamId={booking.eventType?.team?.id}
/>
<AddGuestsDialog
isOpenDialog={isOpenAddGuestsDialog}
setIsOpenDialog={setIsOpenAddGuestsDialog}
bookingId={booking.id}
/>
{booking.paid && booking.payment[0] && (
<ChargeCardDialog
isOpenDialog={chargeCardDialogIsOpen}
@@ -0,0 +1,107 @@
import type { Dispatch, SetStateAction } from "react";
import { useState } from "react";
import { z } from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
MultiEmail,
Icon,
showToast,
} from "@calcom/ui";
interface IAddGuestsDialog {
isOpenDialog: boolean;
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
bookingId: number;
}
export const AddGuestsDialog = (props: IAddGuestsDialog) => {
const { t } = useLocale();
const ZAddGuestsInputSchema = z.array(z.string().email()).refine((emails) => {
const uniqueEmails = new Set(emails);
return uniqueEmails.size === emails.length;
});
const { isOpenDialog, setIsOpenDialog, bookingId } = props;
const utils = trpc.useUtils();
const [multiEmailValue, setMultiEmailValue] = useState<string[]>([""]);
const [isInvalidEmail, setIsInvalidEmail] = useState(false);
const addGuestsMutation = trpc.viewer.bookings.addGuests.useMutation({
onSuccess: async () => {
showToast(t("guests_added"), "success");
setIsOpenDialog(false);
setMultiEmailValue([""]);
utils.viewer.bookings.invalidate();
},
onError: (err) => {
const message = `${err.data?.code}: ${t(err.message)}`;
showToast(message || t("unable_to_add_guests"), "error");
},
});
const handleAdd = () => {
if (multiEmailValue.length === 0) {
return;
}
const validationResult = ZAddGuestsInputSchema.safeParse(multiEmailValue);
if (validationResult.success) {
addGuestsMutation.mutate({ bookingId, guests: multiEmailValue });
} else {
setIsInvalidEmail(true);
}
};
return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent enableOverflow>
<div className="flex flex-row space-x-3">
<div className="bg-subtle flex h-10 w-10 flex-shrink-0 justify-center rounded-full ">
<Icon name="user-plus" className="m-auto h-6 w-6" />
</div>
<div className="w-full pt-1">
<DialogHeader title={t("additional_guests")} />
<MultiEmail
label={t("add_emails")}
value={multiEmailValue}
readOnly={false}
setValue={setMultiEmailValue}
/>
{isInvalidEmail && (
<div className="my-4 flex text-sm text-red-700">
<div className="flex-shrink-0">
<Icon name="triangle-alert" className="h-5 w-5" />
</div>
<div className="ml-3">
<p className="font-medium">{t("emails_must_be_unique_valid")}</p>
</div>
</div>
)}
<DialogFooter>
<Button
onClick={() => {
setMultiEmailValue([""]);
setIsInvalidEmail(false);
setIsOpenDialog(false);
}}
type="button"
color="secondary">
{t("cancel")}
</Button>
<Button data-testid="add_members" loading={addGuestsMutation.isPending} onClick={handleAdd}>
{t("add")}
</Button>
</DialogFooter>
</div>
</div>
</DialogContent>
</Dialog>
);
};
+14 -38
View File
@@ -1,5 +1,5 @@
import type { EventTypeSetupProps } from "pages/event-types/[type]";
import { useEffect, useState } from "react";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form";
import type { MultiValue } from "react-select";
@@ -12,45 +12,10 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import { slugify } from "@calcom/lib/slugify";
import turndown from "@calcom/lib/turndownService";
import {
Label,
Select,
SettingsToggle,
Skeleton,
TextField,
Editor,
SkeletonContainer,
SkeletonText,
} from "@calcom/ui";
import { Label, Select, SettingsToggle, Skeleton, TextField, Editor } from "@calcom/ui";
import Locations from "@components/eventtype/Locations";
const DescriptionEditor = ({ isEditable }: { isEditable: boolean }) => {
const formMethods = useFormContext<FormValues>();
const [mounted, setIsMounted] = useState(false);
const { t } = useLocale();
const [firstRender, setFirstRender] = useState(true);
useEffect(() => {
setIsMounted(true);
}, []);
return mounted ? (
<Editor
getText={() => md.render(formMethods.getValues("description") || "")}
setText={(value: string) => formMethods.setValue("description", turndown(value), { shouldDirty: true })}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
editable={isEditable}
firstRender={firstRender}
setFirstRender={setFirstRender}
/>
) : (
<SkeletonContainer>
<SkeletonText className="block h-24 w-full" />
</SkeletonContainer>
);
};
export const EventSetupTab = (
props: Pick<
EventTypeSetupProps,
@@ -63,6 +28,7 @@ export const EventSetupTab = (
const [multipleDuration, setMultipleDuration] = useState(
formMethods.getValues("metadata")?.multipleDuration
);
const [firstRender, setFirstRender] = useState(true);
const orgBranding = useOrgBranding();
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
@@ -111,7 +77,17 @@ export const EventSetupTab = (
{t("description")}
{(isManagedEventType || isChildrenManagedEventType) && shouldLockIndicator("description")}
</Label>
<DescriptionEditor isEditable={!descriptionLockedProps.disabled} />
<Editor
getText={() => md.render(formMethods.getValues("description") || "")}
setText={(value: string) =>
formMethods.setValue("description", turndown(value), { shouldDirty: true })
}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
editable={!descriptionLockedProps.disabled}
firstRender={firstRender}
setFirstRender={setFirstRender}
/>
</div>
<TextField
required
@@ -330,7 +330,7 @@ function EventTypeSingleLayout({
<>
<div
className={classNames(
"sm:hover:bg-muted hidden cursor-pointer items-center rounded-md",
"sm:hover:bg-muted hidden cursor-pointer items-center rounded-md transition",
formMethods.watch("hidden") ? "pl-2" : "",
"lg:flex"
)}>
@@ -459,7 +459,7 @@ function EventTypeSingleLayout({
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuSeparator />
<div className="hover:bg-subtle flex h-9 cursor-pointer flex-row items-center justify-between px-4 py-2">
<div className="hover:bg-subtle flex h-9 cursor-pointer flex-row items-center justify-between px-4 py-2 transition">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
+1 -1
View File
@@ -31,7 +31,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
<Link
key={member.id}
href={{ pathname: `${member.bookerUrl}/${member.username}`, query: queryParamsToForward }}>
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 transition hover:cursor-pointer">
<UserAvatar noOrganizationIndicator size="md" user={member} />
<section className="mt-2 line-clamp-4 w-full space-y-1">
<p className="text-default font-medium">{member.name}</p>
@@ -413,7 +413,7 @@ export default function Success(props: PageProps) {
<Link
href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}
data-testid="back-to-bookings"
className="hover:bg-subtle text-subtle hover:text-default mt-2 inline-flex px-1 py-2 text-sm dark:hover:bg-transparent">
className="hover:bg-subtle text-subtle hover:text-default mt-2 inline-flex px-1 py-2 text-sm transition dark:hover:bg-transparent">
<Icon name="chevron-left" className="h-5 w-5 rtl:rotate-180" /> {t("back_to_bookings")}
</Link>
</div>
@@ -739,7 +739,7 @@ export const EventTypeList = ({
)}
<DropdownMenuSeparator />
{!isManagedEventType && (
<div className="hover:bg-subtle flex h-9 cursor-pointer flex-row items-center justify-between px-4 py-2">
<div className="hover:bg-subtle flex h-9 cursor-pointer flex-row items-center justify-between px-4 py-2 transition">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
@@ -1280,7 +1280,7 @@ export const InfiniteEventTypeList = ({
)}
<DropdownMenuSeparator />
{!isManagedEventType && (
<div className="hover:bg-subtle flex h-9 cursor-pointer flex-row items-center justify-between px-4 py-2">
<div className="hover:bg-subtle flex h-9 cursor-pointer flex-row items-center justify-between px-4 py-2 transition">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
@@ -145,7 +145,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
length: eventData.length,
metadata: {
...eventData.metadata,
multipleDuration: [15, 30, 60],
multipleDuration: [15, 30, 45, 60, 90],
},
},
user: usernames.join("+"),
+51 -21
View File
@@ -23,6 +23,18 @@ const BUTTONS = {
iconPath: TRANSCRIPTION_STOPPED_ICON,
iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON,
},
WAIT_FOR_TRANSCRIPTION_TO_START: {
label: "Starting..",
tooltip: "Please wait while we start transcription",
iconPath: TRANSCRIPTION_STOPPED_ICON,
iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON,
},
WAIT_FOR_TRANSCRIPTION_TO_STOP: {
label: "Stopping..",
tooltip: "Please wait while we stop transcription",
iconPath: TRANSCRIPTION_STOPPED_ICON,
iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON,
},
START_RECORDING: {
label: "Record",
tooltip: "Start recording",
@@ -35,6 +47,12 @@ const BUTTONS = {
iconPath: RECORDING_DEFAULT_ICON,
iconPathDarkMode: RECORDING_DEFAULT_ICON,
},
WAIT_FOR_RECORDING_TO_STOP: {
label: "Stopping..",
tooltip: "Please wait while we stop recording",
iconPath: RECORDING_DEFAULT_ICON,
iconPathDarkMode: RECORDING_DEFAULT_ICON,
},
STOP_RECORDING: {
label: "Stop",
tooltip: "Stop recording",
@@ -93,6 +111,12 @@ export const CalAiTranscribe = () => {
const toggleRecording = async () => {
if (recording?.isRecording) {
daily?.updateCustomTrayButtons({
recording: BUTTONS.WAIT_FOR_RECORDING_TO_STOP,
transcription: transcription?.isTranscribing
? BUTTONS.STOP_TRANSCRIPTION
: BUTTONS.START_TRANSCRIPTION,
});
await daily?.stopRecording();
} else {
daily?.updateCustomTrayButtons({
@@ -111,8 +135,16 @@ export const CalAiTranscribe = () => {
const toggleTranscription = async () => {
if (transcription?.isTranscribing) {
daily?.updateCustomTrayButtons({
recording: recording?.isRecording ? BUTTONS.STOP_RECORDING : BUTTONS.START_RECORDING,
transcription: BUTTONS.WAIT_FOR_TRANSCRIPTION_TO_STOP,
});
daily?.stopTranscription();
} else {
daily?.updateCustomTrayButtons({
recording: recording?.isRecording ? BUTTONS.STOP_RECORDING : BUTTONS.START_RECORDING,
transcription: BUTTONS.WAIT_FOR_TRANSCRIPTION_TO_START,
});
daily?.startTranscription();
}
};
@@ -146,25 +178,23 @@ export const CalAiTranscribe = () => {
});
}, [transcriptHeight]);
return (
<>
<div
id="cal-ai-transcription"
style={{
textShadow: "0 0 20px black, 0 0 20px black, 0 0 20px black",
backgroundColor: "rgba(0,0,0,0.6)",
}}
ref={transcriptRef}
className="flex max-h-full justify-center overflow-x-hidden overflow-y-scroll p-2 text-center text-white">
{transcript
? transcript.split("\n").map((line, i) => (
<Fragment key={`transcript-${i}`}>
{i > 0 && <br />}
{line}
</Fragment>
))
: ""}
</div>
</>
);
return transcript ? (
<div
id="cal-ai-transcription"
style={{
textShadow: "0 0 20px black, 0 0 20px black, 0 0 20px black",
backgroundColor: "rgba(0,0,0,0.6)",
}}
ref={transcriptRef}
className="flex max-h-full justify-center overflow-x-hidden overflow-y-scroll p-2 text-center text-white">
{transcript
? transcript.split("\n").map((line, i) => (
<Fragment key={`transcript-${i}`}>
{i > 0 && <br />}
{line}
</Fragment>
))
: ""}
</div>
) : null;
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "4.4.1",
"version": "4.4.2",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+2 -2
View File
@@ -84,7 +84,7 @@ function TeamPage({
<li
key={index}
className={classNames(
"dark:bg-darkgray-100 bg-default hover:bg-muted border-subtle group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0",
"dark:bg-darkgray-100 bg-default hover:bg-muted border-subtle group relative border-b transition first:rounded-t-md last:rounded-b-md last:border-b-0",
!isEmbed && "bg-default"
)}>
<div className="px-6 py-4 ">
@@ -129,7 +129,7 @@ function TeamPage({
(mem) => mem.subteams?.includes(ch.slug) && mem.accepted
).length;
return (
<li key={i} className="hover:bg-muted w-full rounded-md">
<li key={i} className="hover:bg-muted w-full rounded-md transition">
<Link href={`/${ch.slug}`} className="flex items-center justify-between">
<div className="flex items-center px-5 py-5">
<div className="ms-3 inline-block truncate">
@@ -72,23 +72,23 @@ test("dynamic booking info prefilled by query params", async ({ page, users }) =
await page.waitForLoadState("networkidle");
const badgeByDurationTestId = (duration: number) => `multiple-choice-${duration}mins`;
const listItemByDurationTestId = (duration: number) => `multiple-choice-${duration}mins`;
let badgeLocator = await page.getByTestId(badgeByDurationTestId(duration));
let activeState = await badgeLocator.getAttribute("data-active");
let listItemLocator = await page.getByTestId(listItemByDurationTestId(duration));
let activeState = await listItemLocator.getAttribute("data-active");
expect(activeState).toEqual("true");
duration = 30;
await page.goto(`/${pro.username}+${free.username}?duration=${duration}`);
badgeLocator = await page.getByTestId(badgeByDurationTestId(duration));
activeState = await badgeLocator.getAttribute("data-active");
listItemLocator = await page.getByTestId(listItemByDurationTestId(duration));
activeState = await listItemLocator.getAttribute("data-active");
expect(activeState).toEqual("true");
// Check another badge just to ensure its not selected
badgeLocator = await page.getByTestId(badgeByDurationTestId(15));
activeState = await badgeLocator.getAttribute("data-active");
listItemLocator = await page.getByTestId(listItemByDurationTestId(15));
activeState = await listItemLocator.getAttribute("data-active");
expect(activeState).toEqual("false");
});
// eslint-disable-next-line playwright/no-skipped-test
+1
View File
@@ -64,6 +64,7 @@ testBothFutureAndLegacyRoutes.describe("Event Types tests", () => {
const nonce = randomString(3);
const eventTitle = `my recurring event ${nonce}`;
await createNewEventType(page, { eventTitle });
await page.waitForLoadState("networkidle");
await page.click("[data-testid=vertical-tab-recurring]");
await expect(page.locator("[data-testid=recurring-event-collapsible]")).toBeHidden();
+10 -6
View File
@@ -121,7 +121,7 @@ test.describe("Insights", async () => {
.locator('div[class="flex flex-col gap-0.5 [&>*:first-child]:mt-1 [&>*:last-child]:mb-1"]')
.click();
const teamSelectFilter = await page.locator(
'div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 hover:cursor-pointer"]'
'div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 transition hover:cursor-pointer"]'
);
await expect(teamSelectFilter).toHaveCount(3);
@@ -142,7 +142,7 @@ test.describe("Insights", async () => {
await page.getByTestId("dashboard-shell").getByText("All").nth(1).click();
const teamSelectFilter = await page.locator(
'div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 hover:cursor-pointer"]'
'div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 transition hover:cursor-pointer"]'
);
await expect(teamSelectFilter).toHaveCount(4);
@@ -164,7 +164,7 @@ test.describe("Insights", async () => {
.locator('div[class="flex flex-col gap-0.5 [&>*:first-child]:mt-1 [&>*:last-child]:mb-1"]')
.click();
const teamSelectFilter = await page.locator(
'div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 hover:cursor-pointer"]'
'div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 transition hover:cursor-pointer"]'
);
await expect(teamSelectFilter).toHaveCount(3);
@@ -186,7 +186,7 @@ test.describe("Insights", async () => {
.locator('div[class="flex flex-col gap-0.5 [&>*:first-child]:mt-1 [&>*:last-child]:mb-1"]')
.click();
const teamSelectFilter = await page.locator(
'div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 hover:cursor-pointer"]'
'div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 transition hover:cursor-pointer"]'
);
await expect(teamSelectFilter).toHaveCount(3);
@@ -218,13 +218,17 @@ test.describe("Insights", async () => {
await page.locator('div[class="flex select-none truncate font-medium"]').getByText("People").click();
await page
.locator('div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 hover:cursor-pointer"]')
.locator(
'div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 transition hover:cursor-pointer"]'
)
.nth(0)
.click();
await page.waitForLoadState("networkidle");
await page
.locator('div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 hover:cursor-pointer"]')
.locator(
'div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 transition hover:cursor-pointer"]'
)
.nth(1)
.click();
await page.waitForLoadState("networkidle");
@@ -654,6 +654,7 @@
"default_duration": "Výchozí doba trvání",
"default_duration_no_options": "Nejprve vyberte dostupné doby trvání",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "min",
"round_robin": "Plánování Round Robin",
"round_robin_description": "Schůzky v řadě mezi několika členy týmu.",
@@ -554,6 +554,7 @@
"default_duration": "Standard varighed",
"default_duration_no_options": "Vælg venligst tilgængelige varigheder først",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Minutter",
"round_robin": "Round Robin",
"round_robin_description": "Cyklusmøder mellem flere teammedlemmer.",
@@ -713,6 +713,7 @@
"default_duration_no_options": "Please choose available durations first",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"multiple_duration_timeUnit_short": "{{count}}$t({{unit}}_short)",
"minutes": "Minutes",
"use_cal_ai_to_make_call_description": "Use Cal.ai to get an AI powered phone number or make calls to guests.",
"round_robin": "Round Robin",
@@ -1118,7 +1119,9 @@
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
"impersonating_stop_instructions": "Click here to stop",
"event_location_changed": "Updated - Your event changed the location",
"new_guests_added": "Added - New guests added to your event",
"location_changed_event_type_subject": "Location Changed: {{eventType}} with {{name}} at {{date}}",
"guests_added_event_type_subject": "Guests Added: {{eventType}} with {{name}} at {{date}}",
"current_location": "Current Location",
"new_location": "New Location",
"session": "Session",
@@ -1130,7 +1133,10 @@
"set_location": "Set Location",
"update_location": "Update Location",
"location_updated": "Location updated",
"guests_added": "Guests added",
"unable_to_add_guests": "Unable to add guests",
"email_validation_error": "That doesn't look like an email address",
"emails_must_be_unique_valid": "Emails must be unique and valid",
"url_validation_error": "That doesn't look like a URL",
"place_where_cal_widget_appear": "Place this code in your HTML where you want your {{appName}} widget to appear.",
"create_update_react_component": "Create or update an existing React component as shown below.",
@@ -1224,6 +1230,8 @@
"day_timeUnit": "days",
"hour_timeUnit": "hours",
"minute_timeUnit": "mins",
"minute_short": "m",
"hour_short": "h",
"new_workflow_heading": "Create your first workflow",
"new_workflow_description": "Workflows enable you to automate sending reminders and notifications.",
"active_on": "Active on",
@@ -1277,8 +1285,10 @@
"reminder_email": "Reminder: {{eventType}} with {{name}} at {{date}}",
"not_triggering_existing_bookings": "Won't trigger for already existing bookings as user will be asked for phone number when booking the event.",
"minute_one": "{{count}} minute",
"minute_one_short": "{{count}}m",
"minute_other": "{{count}} minutes",
"hour_one": "{{count}} hour",
"hour_one_short": "{{count}}h",
"hour_other": "{{count}} hours",
"invalid_input": "Invalid input",
"broken_video_action": "We could not add the <1>{{location}}</1> meeting link to your scheduled event. Contact your invitees or update your calendar event to add the details. You can either <3> change your location on the event type </3> or try <5>removing and adding the app again.</5>",
@@ -2421,6 +2431,7 @@
"primary": "Primary",
"make_primary": "Make primary",
"add_email": "Add Email",
"add_emails": "Add Emails",
"add_email_description": "Add an email address to replace your primary or to use as an alternative email on your event types.",
"confirm_email": "Confirm your email",
"scheduler_first_name": "The first name of the person booking",
@@ -2561,5 +2572,6 @@
"number_of_options":"{{count}} options",
"reschedule_with_same_round_robin_host_title": "Reschedule with same Round-Robin host",
"reschedule_with_same_round_robin_host_description": "Rescheduled events will be assigned to the same host as initially scheduled",
"disable_input_if_prefilled": "Disable input if the URL identifier is prefilled",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -653,6 +653,7 @@
"default_duration": "Duración predeterminada",
"default_duration_no_options": "Seleccione primero las duraciones disponibles",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Minutos",
"round_robin": "Petición Firmada por Turnos",
"round_robin_description": "Ciclo de reuniones entre varios miembros del equipo.",
@@ -616,6 +616,8 @@
"day_timeUnit": "egun",
"hour_timeUnit": "ordu",
"minute_timeUnit": "minutu",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"current": "Unekoa",
"confirm_username_change_dialog_title": "Baieztatu erabiltzaile-izenaren aldaketa",
"requires_confirmation": "Baieztapena behar du",
@@ -654,6 +654,7 @@
"default_duration": "デフォルトの時間",
"default_duration_no_options": "最初に空いている時間を選択してください",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "分",
"round_robin": "ラウンドロビン",
"round_robin_description": "複数のチームメンバー間のミーティングを定期化する。",
@@ -654,6 +654,7 @@
"default_duration": "기본 기간",
"default_duration_no_options": "사용 가능한 기간을 먼저 선택하세요",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "분",
"round_robin": "라운드 로빈",
"round_robin_description": "여러 팀 구성원 간의 주기적인 회의",
@@ -654,6 +654,7 @@
"default_duration": "Standaardduur",
"default_duration_no_options": "Kies eerst de beschikbare duur",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Minuten",
"round_robin": "Round Robin",
"round_robin_description": "Afspraken wisselen tussen meerdere teamleden.",
@@ -545,6 +545,7 @@
"default_duration": "Standard varighet",
"default_duration_no_options": "Vennligst velg tilgjengelige varigheter først",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Minutter",
"round_robin": "Round Robin",
"round_robin_description": "Varier møter mellom flere teammedlemmer.",
@@ -654,6 +654,7 @@
"default_duration": "Domyślny czas trwania",
"default_duration_no_options": "Proszę najpierw wybrać dostępne czasy trwania",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Minuty",
"round_robin": "Round Robin",
"round_robin_description": "Cykl spotkań między wieloma członkami zespołu.",
@@ -699,6 +699,7 @@
"default_duration": "Duração padrão",
"default_duration_no_options": "Escolha as durações disponíveis primeiro",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Minutos",
"use_cal_ai_to_make_call_description": "Use Cal.ai para adquirir um número de telefone com IA ou faça ligações para convidados.",
"round_robin": "Round Robin",
@@ -654,6 +654,7 @@
"default_duration": "Durată implicită",
"default_duration_no_options": "Alege mai întâi duratele disponibile",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Minute",
"round_robin": "Round Robin",
"round_robin_description": "Întâlniri ciclice între mai mulţi membri ai echipei.",
@@ -655,6 +655,7 @@
"default_duration": "Продолжительность по умолчанию",
"default_duration_no_options": "Пожалуйста, выберите сначала доступную продолжительность",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "мин.",
"round_robin": "По кругу",
"round_robin_description": "Цикл встреч между несколькими членами команды.",
@@ -654,6 +654,7 @@
"default_duration": "Podrazumevano trajanje",
"default_duration_no_options": "Najpre izaberite dostupna trajanja",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Minuti",
"round_robin": "Round Robin",
"round_robin_description": "Ciklusirajte sastanke između više članova tima.",
@@ -654,6 +654,7 @@
"default_duration": "Standardvaraktighet",
"default_duration_no_options": "Välj först tillgänglig varaktighet",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Minuter",
"round_robin": "Tur och ordning",
"round_robin_description": "Variera möten mellan olika teammedlemmar.",
@@ -654,6 +654,7 @@
"default_duration": "Varsayılan süre",
"default_duration_no_options": "Lütfen önce uygun süreleri seçin",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Dakika",
"round_robin": "Round Robin",
"round_robin_description": "Toplantıları birden fazla ekip üyesi arasında döndürün.",
@@ -686,6 +686,7 @@
"default_duration": "Тривалість за замовчуванням",
"default_duration_no_options": "Спочатку виберіть доступні варіанти тривалості",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Хвилини",
"round_robin": "Ротація",
"round_robin_description": "Кілька учасників команди призначаються для нарад циклічно й по черзі.",
@@ -654,7 +654,8 @@
"available_durations": "Khoảng thời gian khả dụng",
"default_duration": "Khoảng thời gian mặc định",
"default_duration_no_options": "Vui lòng chọn trước tiên những khoảng thời gian khả dụng",
"multiple_duration_mins": "{{count}}$t(minute_timeUnit)",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Phút",
"round_robin": "Round Robin",
"round_robin_description": "Luân phiên những cuộc họp giữa các thành viên trong nhóm.",
@@ -691,6 +691,7 @@
"default_duration": "默认时长",
"default_duration_no_options": "请先选择可用时长",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "分钟",
"use_cal_ai_to_make_call_description": "使用 Cal.ai 获得人工智能驱动的电话号码或拨打电话给访客。",
"round_robin": "轮流模式",
@@ -660,6 +660,7 @@
"default_duration": "預設時間長度",
"default_duration_no_options": "請先選擇可預約的時間長度",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "分鐘",
"round_robin": "循環制",
"round_robin_description": "與多位團隊成員進行週期會議。",
@@ -5,6 +5,7 @@ import { Controller, useForm } from "react-hook-form";
import { Toaster } from "react-hot-toast";
import z from "zod";
import { WebAppURL } from "@calcom/lib/WebAppURL";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { Button, Form, showToast, TextField } from "@calcom/ui";
@@ -66,7 +67,7 @@ export default function CloseComSetup() {
form={form}
handleSubmit={async (values) => {
const { returnTo } = query;
const url = new URL("/api/integrations/closecom/add");
const url = new WebAppURL("/api/integrations/closecom/add");
if (returnTo) url.searchParams.append("returnTo", `${returnTo}`);
const res = await fetch(url.href, {
method: "POST",
@@ -18,7 +18,7 @@ const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({ eventTyp
<a download href={QR_URL} target="_blank" rel="noreferrer">
<img
className={classNames(
"hover:bg-muted border-default border hover:shadow-sm",
"hover:bg-muted border-default border transition hover:shadow-sm",
size >= 256 && "min-h-32"
)}
style={{ padding: size / 16, borderRadius: size / 20 }}
@@ -465,7 +465,7 @@ export const FormAction = forwardRef(function FormAction<T extends typeof Button
<div
{...restProps}
className={classNames(
"sm:hover:bg-subtle self-center rounded-md p-2 hover:bg-gray-200",
"sm:hover:bg-subtle self-center rounded-md p-2 transition hover:bg-gray-200",
extraClassNames
)}>
<Switch
@@ -47,7 +47,7 @@ export default function FormInputFields(props: Props) {
</label>
</div>
<Component
value={response[field.id]?.value}
value={response[field.id]?.value ?? ""}
placeholder={field.placeholder ?? ""}
// required property isn't accepted by query-builder types
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -207,7 +207,7 @@ const Actions = ({
action="toggle"
routingForm={form}
label="Disable Form"
extraClassNames="hover:bg-subtle cursor-pointer rounded-[5px] pr-4"
extraClassNames="hover:bg-subtle cursor-pointer rounded-[5px] pr-4 transition"
/>
</div>
</FormActionsDropdown>
@@ -0,0 +1,103 @@
import { render, screen } from "@testing-library/react";
import { vi } from "vitest";
import widgets from "./widgets";
const { SelectWidget, MultiSelectWidget } = widgets;
// Mock the dynamic import of Select component
vi.mock("next/dynamic", () => ({
__esModule: true,
default: () => {
return function MockSelect({
options,
onChange,
value,
isMulti,
}: {
options: { value: string; label: string }[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onChange: (value: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
isMulti: boolean;
}) {
return (
<select
data-testid="mock-select"
multiple={isMulti}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value={isMulti ? value.map((v: any) => v.value) : value?.value}
onChange={(e) => {
const selectedOptions = Array.from(e.target.selectedOptions, (option) => ({
value: option.value,
label: option.text,
}));
onChange(isMulti ? selectedOptions : selectedOptions[0]);
}}>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
};
},
}));
describe("Select Widgets", () => {
describe("SelectWidget", () => {
const listValues = [
{ title: "Option 1", value: "1" },
{ title: "Option 2", value: "2" },
{ title: "Option 3", value: "3" },
];
it("should handle render the value in option correctly", () => {
const setValue = vi.fn();
render(<SelectWidget value="2" setValue={setValue} listValues={listValues} />);
const select = screen.getByTestId("mock-select");
expect(select).toBeInTheDocument();
expect(screen.getAllByRole("option")).toHaveLength(3);
expect(setValue).not.toHaveBeenCalled();
});
it("should handle a value that is not in the list and reset the value to empty string", () => {
const setValue = vi.fn();
render(<SelectWidget value="4" setValue={setValue} listValues={listValues} />);
const select = screen.getByTestId("mock-select");
expect(select).toBeInTheDocument();
expect(screen.getAllByRole("option")).toHaveLength(3);
expect(setValue).toHaveBeenCalledWith("");
});
});
describe("MultiSelectWidget", () => {
const listValues = [
{ title: "Option 1", value: "1" },
{ title: "Option 2", value: "2" },
{ title: "Option 3", value: "3" },
];
it("renders options correctly", () => {
const setValue = vi.fn();
render(<MultiSelectWidget value={[]} setValue={setValue} listValues={listValues} />);
const select = screen.getByTestId("mock-select");
expect(select).toBeInTheDocument();
expect(screen.getAllByRole("option")).toHaveLength(3);
expect(setValue).not.toHaveBeenCalled();
});
it("sets value to empty array when no options match", () => {
const setValue = vi.fn();
render(<MultiSelectWidget value={["4", "5"]} setValue={setValue} listValues={listValues} />);
expect(setValue).toHaveBeenCalledWith([]);
});
});
});
@@ -150,8 +150,6 @@ const MultiSelectWidget = ({
value,
...remainingProps
}: SelectLikeComponentPropsRAQB<string[]>) => {
//TODO: Use Select here.
//TODO: Let's set listValue itself as label and value instead of using title.
if (!listValues) {
return null;
}
@@ -164,6 +162,14 @@ const MultiSelectWidget = ({
const optionsFromList = selectItems.filter((item) => value?.includes(item.value));
// If no value could be found in the list, then we set the value to undefined.
// This is to update the value back to the source that we couldn't set it. This is important otherwise the outside party thinks that the value is set but it is not.
// Do it only when it is not already empty, this is to avoid infinite state updates
// NOTE: value is some times sent as undefined even though the type will tell you that it can't be
if (optionsFromList.length === 0 && value?.length) {
setValue([]);
}
return (
<Select
aria-label="multi-select-dropdown"
@@ -192,6 +198,13 @@ function SelectWidget({ listValues, setValue, value, ...remainingProps }: Select
});
const optionFromList = selectItems.find((item) => item.value === value);
// If the value is not in the list, then we set the value to undefined.
// This is to update the value back to the source that we couldn't set it. This is important otherwise the outside party thinks that the value is set but it is not.
// Do it only when it is not already empty string, this is to avoid infinite state updates
if (!optionFromList && value) {
setValue("");
}
return (
<Select
aria-label="select-dropdown"
+29 -1
View File
@@ -18,6 +18,7 @@ import type { EmailVerifyLink } from "./templates/account-verify-email";
import AccountVerifyEmail from "./templates/account-verify-email";
import type { OrganizationNotification } from "./templates/admin-organization-notification";
import AdminOrganizationNotification from "./templates/admin-organization-notification";
import AttendeeAddGuestsEmail from "./templates/attendee-add-guests-email";
import AttendeeAwaitingPaymentEmail from "./templates/attendee-awaiting-payment-email";
import AttendeeCancelledEmail from "./templates/attendee-cancelled-email";
import AttendeeCancelledSeatEmail from "./templates/attendee-cancelled-seat-email";
@@ -48,6 +49,7 @@ import type { OrganizationCreation } from "./templates/organization-creation-ema
import OrganizationCreationEmail from "./templates/organization-creation-email";
import type { OrganizationEmailVerify } from "./templates/organization-email-verification";
import OrganizationEmailVerification from "./templates/organization-email-verification";
import OrganizerAddGuestsEmail from "./templates/organizer-add-guests-email";
import OrganizerAttendeeCancelledSeatEmail from "./templates/organizer-attendee-cancelled-seat-email";
import OrganizerCancelledEmail from "./templates/organizer-cancelled-email";
import OrganizerDailyVideoDownloadRecordingEmail from "./templates/organizer-daily-video-download-recording-email";
@@ -378,7 +380,7 @@ export const sendCancelledEmails = async (
t: attendee.language.translate,
attendeeName: attendee.name,
host: calendarEvent.organizer.name,
eventType: calendarEvent.type,
eventType: calendarEvent.title,
eventDuration,
...(calendarEvent.responses && { bookingFields: calendarEvent.responses }),
...(calendarEvent.location && { location: calendarEvent.location }),
@@ -520,6 +522,32 @@ export const sendLocationChangeEmails = async (
await Promise.all(emailsToSend);
};
export const sendAddGuestsEmails = async (calEvent: CalendarEvent, newGuests: string[]) => {
const calendarEvent = formatCalEvent(calEvent);
const emailsToSend: Promise<unknown>[] = [];
emailsToSend.push(sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent })));
if (calendarEvent.team?.members) {
for (const teamMember of calendarEvent.team.members) {
emailsToSend.push(
sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent, teamMember }))
);
}
}
emailsToSend.push(
...calendarEvent.attendees.map((attendee) => {
if (newGuests.includes(attendee.email)) {
return sendEmail(() => new AttendeeScheduledEmail(calendarEvent, attendee));
} else {
return sendEmail(() => new AttendeeAddGuestsEmail(calendarEvent, attendee));
}
})
);
await Promise.all(emailsToSend);
};
export const sendFeedbackEmail = async (feedback: Feedback) => {
await sendEmail(() => new FeedbackEmail(feedback));
};
@@ -0,0 +1,10 @@
import { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";
export const AttendeeAddGuestsEmail = (props: React.ComponentProps<typeof AttendeeScheduledEmail>) => (
<AttendeeScheduledEmail
title="new_guests_added"
headerType="calendarCircle"
subject="guests_added_event_type_subject"
{...props}
/>
);
@@ -0,0 +1,11 @@
import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
export const OrganizerAddGuestsEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => (
<OrganizerScheduledEmail
title="new_guests_added"
headerType="calendarCircle"
subject="guests_added_event_type_subject"
callToAction={null}
{...props}
/>
);
+2
View File
@@ -35,4 +35,6 @@ export { AdminOrganizationNotificationEmail } from "./AdminOrganizationNotificat
export { BookingRedirectEmailNotification } from "./BookingRedirectEmailNotification";
export { VerifyEmailChangeEmail } from "./VerifyEmailChangeEmail";
export { OrganizationCreationEmail } from "./OrganizationCreationEmail";
export { OrganizerAddGuestsEmail } from "./OrganizerAddGuestsEmail";
export { AttendeeAddGuestsEmail } from "./AttendeeAddGuestsEmail";
export { OrganizationAdminNoSlotsEmail } from "./OrganizationAdminNoSlots";
@@ -0,0 +1,34 @@
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeAddGuestsEmail extends AttendeeScheduledEmail {
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("new_guests_added"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: "attendee",
status: "CONFIRMED",
}),
method: "REQUEST",
},
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
subject: `${this.t("guests_added_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: this.getFormattedDate(),
})}`,
html: await renderEmail("AttendeeAddGuestsEmail", {
calEvent: this.calEvent,
attendee: this.attendee,
}),
text: this.getTextBody("new_guests_added"),
};
}
}
@@ -0,0 +1,38 @@
import { APP_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerAddGuestsEmail extends OrganizerScheduledEmail {
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("new_guests_added"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: "organizer",
status: "CONFIRMED",
}),
method: "REQUEST",
},
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
subject: `${this.t("guests_added_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: this.getFormattedDate(),
})}`,
html: await renderEmail("OrganizerAddGuestsEmail", {
attendee: this.calEvent.organizer,
calEvent: this.calEvent,
}),
text: this.getTextBody("new_guests_added"),
};
}
}
@@ -68,7 +68,7 @@ export const getEmbedIframe = async ({
}, hardTimeout);
});
},
!process.env.CI ? 150000 : 15000
!process.env.CI ? 15000 : 15000
);
if (!iframeReady) {
return null;
+12 -10
View File
@@ -315,16 +315,18 @@ const AdminAppsListContainer = () => {
/>
))}
</List>
<EditKeysModal
keys={modalState.keys}
dirName={modalState.dirName}
handleModelClose={handleModelClose}
isOpen={modalState.isOpen === "editKeys"}
slug={modalState.slug}
type={modalState.type}
fromEnabled={modalState.fromEnabled}
appName={modalState.appName}
/>
{modalState.isOpen === "editKeys" && (
<EditKeysModal
keys={modalState.keys}
dirName={modalState.dirName}
handleModelClose={handleModelClose}
isOpen={modalState.isOpen === "editKeys"}
slug={modalState.slug}
type={modalState.type}
fromEnabled={modalState.fromEnabled}
appName={modalState.appName}
/>
)}
</>
);
};
@@ -169,7 +169,7 @@ export const EventMeta = ({
timezoneSelectCustomClassname={classNames?.eventMetaTimezoneSelect}
classNames={{
control: () => "!min-h-0 p-0 w-full border-0 bg-transparent focus-within:ring-0",
menu: () => "!w-64 max-w-[90vw]",
menu: () => "!w-64 max-w-[90vw] mb-1 ",
singleValue: () => "text-text py-1",
indicatorsContainer: () => "ml-auto",
container: () => "max-w-full",
@@ -142,7 +142,7 @@ export const EventDetails = ({ event, blocks = defaultEventDetailsBlocks }: Even
switch (block) {
case EventDetailBlocks.DURATION:
return (
<EventMetaBlock key={block} icon="clock">
<EventMetaBlock key={block} icon="clock" className="items-center">
<EventDuration event={event} />
</EventMetaBlock>
);
@@ -1,12 +1,12 @@
import type { TFunction } from "next-i18next";
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { useIsPlatform } from "@calcom/atoms/monorepo";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import type { BookerEvent } from "@calcom/features/bookings/types";
import classNames from "@calcom/lib/classNames";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge } from "@calcom/ui";
import { useShouldShowArrows, Icon } from "@calcom/ui";
/** Render X mins as X hours or X hours Y mins instead of in minutes once >= 60 minutes */
export const getDurationFormatted = (mins: number | undefined, t: TFunction) => {
@@ -19,16 +19,16 @@ export const getDurationFormatted = (mins: number | undefined, t: TFunction) =>
if (mins > 0) {
minStr =
mins === 1
? t("minute_one", { count: 1 })
: t("multiple_duration_timeUnit", { count: mins, unit: "minute" });
? t("minute_one_short", { count: 1 })
: t("multiple_duration_timeUnit_short", { count: mins, unit: "minute" });
}
// format hours string
let hourStr = "";
if (hours > 0) {
hourStr =
hours === 1
? t("hour_one", { count: 1 })
: t("multiple_duration_timeUnit", { count: hours, unit: "hour" });
? t("hour_one_short", { count: 1 })
: t("multiple_duration_timeUnit_short", { count: hours, unit: "hour" });
}
if (hourStr && minStr) return `${hourStr} ${minStr}`;
@@ -41,6 +41,7 @@ export const EventDuration = ({
event: Pick<BookerEvent, "length" | "metadata" | "isDynamic">;
}) => {
const { t } = useLocale();
const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
const isPlatform = useIsPlatform();
const [selectedDuration, setSelectedDuration, state] = useBookerStore((state) => [
state.selectedDuration,
@@ -48,6 +49,20 @@ export const EventDuration = ({
state.state,
]);
const { ref, calculateScroll, leftVisible, rightVisible } = useShouldShowArrows();
const handleLeft = () => {
if (ref.current) {
ref.current.scrollLeft -= 100;
}
};
const handleRight = () => {
if (ref.current) {
ref.current.scrollLeft += 100;
}
};
const isDynamicEvent = "isDynamic" in event && event.isDynamic;
// Sets initial value of selected duration to the default duration.
@@ -57,27 +72,63 @@ export const EventDuration = ({
setSelectedDuration(event.length);
}, [selectedDuration, setSelectedDuration, event.metadata?.multipleDuration, event.length, isDynamicEvent]);
useEffect(() => {
const timeout = setTimeout(() => {
if (selectedDuration && itemRefs.current[selectedDuration]) {
itemRefs.current[selectedDuration]?.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "center",
});
}
}, 100);
return () => clearTimeout(timeout);
}, [selectedDuration]);
if ((!event?.metadata?.multipleDuration && !isDynamicEvent) || isPlatform)
return <>{getDurationFormatted(event.length, t)}</>;
const durations = event?.metadata?.multipleDuration || [15, 30, 60, 90];
return (
<div className="flex flex-wrap gap-2">
{durations
.filter((dur) => state !== "booking" || dur === selectedDuration)
.map((duration) => (
<Badge
data-testId={`multiple-choice-${duration}mins`}
data-active={selectedDuration === duration ? "true" : "false"}
variant="gray"
className={classNames(selectedDuration === duration && "bg-brand-default text-brand")}
size="md"
key={duration}
onClick={() => setSelectedDuration(duration)}>
{getDurationFormatted(duration, t)}
</Badge>
))}
return selectedDuration ? (
<div className="border-default relative mr-5 flex flex-row items-center justify-between rounded-md border">
{leftVisible && (
<button onClick={handleLeft} className="absolute bottom-0 left-0 flex">
<div className="bg-default flex h-9 w-5 items-center justify-end rounded-md">
<Icon name="chevron-left" className="text-subtle h-4 w-4" />
</div>
<div className="to-default flex h-9 w-5 bg-gradient-to-l from-transparent" />
</button>
)}
<ul
className="bg-default no-scrollbar flex max-w-full items-center gap-0.5 overflow-x-auto rounded-md p-1"
onScroll={(e) => calculateScroll(e)}
ref={ref}>
{durations
.filter((dur) => state !== "booking" || dur === selectedDuration)
.map((duration, index) => (
<li
data-testId={`multiple-choice-${duration}mins`}
data-active={selectedDuration === duration ? "true" : "false"}
key={index}
onClick={() => setSelectedDuration(duration)}
ref={(el) => (itemRefs.current[duration] = el)}
className={classNames(
selectedDuration === duration ? "bg-emphasis" : "hover:text-emphasis",
"text-default cursor-pointer rounded-[4px] px-3 py-1.5 text-sm leading-tight transition"
)}>
<div className="w-max">{getDurationFormatted(duration, t)}</div>
</li>
))}
</ul>
{rightVisible && (
<button onClick={handleRight} className="absolute bottom-0 right-0 flex">
<div className="to-default flex h-9 w-5 bg-gradient-to-r from-transparent" />
<div className="bg-default flex h-9 w-5 items-center justify-end rounded-md">
<Icon name="chevron-right" className="text-subtle h-4 w-4" />
</div>
</button>
)}
</div>
);
) : null;
};
@@ -483,15 +483,8 @@ async function handler(
if (eventType.seatsPerTimeSlot) {
const booking = await prisma.booking.findFirst({
where: {
OR: [
{
uid: rescheduleUid || reqBody.bookingUid,
},
{
eventTypeId: eventType.id,
startTime: new Date(dayjs(reqBody.start).utc().format()),
},
],
eventTypeId: eventType.id,
startTime: new Date(dayjs(reqBody.start).utc().format()),
status: BookingStatus.ACCEPTED,
},
});
@@ -2413,6 +2413,151 @@ describe("handleSeats", () => {
// const rescheduledBooking = await handleNewBooking(req);
await expect(() => handleNewBooking(req)).rejects.toThrowError(ErrorCode.NotEnoughAvailableSeats);
});
test("When trying to reschedule in a non-available slot, throw an error", async () => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
});
const firstBookingId = 1;
const firstBookingUid = "abc123";
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const firstBookingStartTime = `${plus1DateString}T04:00:00Z`;
const firstBookingEndTime = `${plus1DateString}T04:30:00Z`;
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
// Non-available time slot choosen (7:30PM - 8:00PM IST) while rescheduling
const secondBookingStartTime = `${plus2DateString}T14:00:00Z`;
const secondBookingEndTime = `${plus2DateString}T14:30:00Z`;
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slug: "seated-event",
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
seatsPerTimeSlot: 3,
seatsShowAttendees: false,
},
],
bookings: [
{
id: firstBookingId,
uid: firstBookingUid,
eventTypeId: 1,
userId: organizer.id,
status: BookingStatus.ACCEPTED,
startTime: firstBookingStartTime,
endTime: firstBookingEndTime,
metadata: {
videoCallUrl: "https://existing-daily-video-call-url.example.com",
},
references: [
{
type: appStoreMetadata.dailyvideo.type,
uid: "MOCK_ID",
meetingId: "MOCK_ID",
meetingPassword: "MOCK_PASS",
meetingUrl: "http://mock-dailyvideo.example.com",
credentialId: null,
},
],
attendees: [
getMockBookingAttendee({
id: 1,
name: "Seat 1",
email: "seat1@test.com",
locale: "en",
timeZone: "America/Toronto",
bookingSeat: {
referenceUid: "booking-seat-1",
data: {},
},
noShow: false,
}),
getMockBookingAttendee({
id: 2,
name: "Seat 2",
email: "seat2@test.com",
locale: "en",
timeZone: "America/Toronto",
bookingSeat: {
referenceUid: "booking-seat-2",
data: {},
},
noShow: false,
}),
getMockBookingAttendee({
id: 3,
name: "Seat 3",
email: "seat3@test.com",
locale: "en",
timeZone: "America/Toronto",
bookingSeat: {
referenceUid: "booking-seat-3",
data: {},
},
noShow: false,
}),
],
},
],
organizer,
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
const reqBookingUser = "seatedAttendee";
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: BookingLocations.CalVideo },
},
rescheduleUid: firstBookingUid,
start: secondBookingStartTime,
end: secondBookingEndTime,
user: reqBookingUser,
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
req.userId = organizer.id;
await expect(() => handleNewBooking(req)).rejects.toThrowError(ErrorCode.NoAvailableUsersFound);
});
});
describe("Cancelling a booking", () => {
@@ -69,7 +69,7 @@ function UserToInviteItem({
key={member.userId}
onClick={() => onChange()} // We handle this on click on the div also - for a11y we handle it with label and checkbox below
className={classNames(
"flex cursor-pointer items-center rounded-md px-2 py-1",
"flex cursor-pointer items-center rounded-md px-2 py-1 transition",
isSelected ? "bg-emphasis" : "hover:bg-subtle "
)}>
<div className="flex items-center space-x-2 rtl:space-x-reverse">
@@ -62,7 +62,7 @@ export default function OtherTeamListItem(props: Props) {
return (
<li>
<div className="hover:bg-muted group flex items-center justify-between">
<div className="hover:bg-muted group flex items-center justify-between transition">
{teamInfo}
<div className="px-5 py-5">
<div className="flex space-x-2 rtl:space-x-reverse">
@@ -43,7 +43,7 @@ const AdminAPIViewWrapper = () => {
buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup>
<Button color="primary" href="https://i.cal.com/sales/enterprise" target="_blank">
<Button color="primary" href="https://cal.com/sales" target="_blank">
{t("contact_sales")}
</Button>
<Button color="minimal" href="https://cal.com/enterprise" target="_blank">
@@ -27,15 +27,16 @@ function CreateAttributesPage() {
const utils = trpc.useUtils();
const { t } = useLocale();
// Get the attribute id from the url
const { id } = useParams<{ id: string }>();
const params = useParams<{ id: string }>();
const id = params?.id || "";
// ensure string with zod
const attribute = trpc.viewer.attributes.get.useQuery({ id: id as string });
const attribute = trpc.viewer.attributes.get.useQuery({ id });
const mutation = trpc.viewer.attributes.edit.useMutation({
onSuccess: () => {
showToast(t("attribute_edited_successfully"), "success");
utils.viewer.attributes.get.invalidate({
id: id as string,
id,
});
utils.viewer.attributes.list.invalidate();
router.push("/settings/organizations/attributes");
@@ -59,7 +60,7 @@ function CreateAttributesPage() {
header={<EditAttributeHeader isPending={mutation.isPending} />}
onSubmit={(values) => {
mutation.mutate({
attributeId: id as string,
attributeId: id,
name: values.attrName,
type: values.type,
options: values.options,
@@ -32,7 +32,7 @@ export default function ContactMenuItem(props: ContactMenuItem) {
<a
href={JOIN_COMMUNITY}
target="_blank"
className="hover:bg-subtle hover:text-emphasis text-default flex w-full px-5 py-2 pr-4 text-sm font-medium">
className="hover:bg-subtle hover:text-emphasis text-default flex w-full px-5 py-2 pr-4 text-sm font-medium transition">
{t("community_support")}{" "}
<Icon
name="external-link"
@@ -61,7 +61,7 @@ export default function HelpMenuItem({ onHelpItemSelect }: HelpMenuItemProps) {
onClick={() => onHelpItemSelect()}
href="https://cal.com/docs/"
target="_blank"
className="hover:bg-subtle hover:text-emphasis text-default flex w-full px-5 py-2 pr-4 text-sm font-medium"
className="hover:bg-subtle hover:text-emphasis text-default flex w-full px-5 py-2 pr-4 text-sm font-medium transition"
rel="noreferrer">
{t("documentation")}
<Icon
@@ -22,7 +22,7 @@ export default function FreshChatMenuItem(props: FreshChatMenuItemProps) {
setActive(true);
onHelpItemSelect();
}}
className="hover:bg-subtle hover:text-emphasis text-default flex w-full px-5 py-2 pr-4 text-sm font-medium">
className="hover:bg-subtle hover:text-emphasis text-default flex w-full px-5 py-2 pr-4 text-sm font-medium transition">
{t("contact_support")}
</button>
</>
@@ -27,7 +27,7 @@ export default function HelpscoutMenuItem(props: HelpscoutMenuItemProps) {
<>
<button
onClick={handleClick}
className="hover:bg-subtle hover:text-emphasis text-default flex w-full px-5 py-2 pr-4 text-sm font-medium">
className="hover:bg-subtle hover:text-emphasis text-default flex w-full px-5 py-2 pr-4 text-sm font-medium transition">
{t("contact_support")}
</button>
@@ -24,7 +24,7 @@ export default function ZendeskMenuItem(props: ZendeskMenuItemProps) {
setActive(true);
onHelpItemSelect();
}}
className="hover:bg-subtle hover:text-emphasis text-default flex w-full px-5 py-2 pr-4 text-sm font-medium">
className="hover:bg-subtle hover:text-emphasis text-default flex w-full px-5 py-2 pr-4 text-sm font-medium transition">
{t("contact_support")}
</button>
{active && (
@@ -116,7 +116,7 @@ export default function WorkflowListPage({ workflows }: Props) {
{!(lastItem && lastItem.id === workflow.id) && (
<ArrowButton onClick={() => moveWorkflow(index, 1)} arrowDirection="down" />
)}
<div className="first-line:group hover:bg-muted flex w-full items-center justify-between p-4 sm:px-6">
<div className="first-line:group hover:bg-muted flex w-full items-center justify-between p-4 transition sm:px-6">
<Link href={`/workflows/${workflow.id}`} className="flex-grow cursor-pointer">
<div className="rtl:space-x-reverse">
<div className="flex">
@@ -136,7 +136,7 @@ const Filter = (props: {
return (
<div className={classNames("-mb-2", noFilter ? "w-16" : "w-[100px]")}>
<AnimatedPopover text={noFilter ? "All" : "Filtered"}>
<div className="item-center focus-within:bg-subtle hover:bg-muted flex px-4 py-[6px] hover:cursor-pointer">
<div className="item-center focus-within:bg-subtle hover:bg-muted flex px-4 py-[6px] transition hover:cursor-pointer">
<Avatar
imageSrc={userAvatar || ""}
size="sm"
@@ -171,7 +171,7 @@ const Filter = (props: {
</div>
{teams.map((profile) => (
<div
className="item-center focus-within:bg-subtle hover:bg-muted flex px-4 py-[6px] hover:cursor-pointer"
className="item-center focus-within:bg-subtle hover:bg-muted flex px-4 py-[6px] transition hover:cursor-pointer"
key={`${profile.teamId || 0}`}>
<Avatar
imageSrc={profile.image || ""}
+1 -1
View File
@@ -114,7 +114,7 @@ const ChooseEmbedTypesDialogContent = ({ types }: { types: EmbedTypes }) => {
<div className="items-start space-y-2 md:flex md:space-y-0">
{types.map((embed, index) => (
<button
className="hover:bg-subtle bg-muted w-full self-stretch rounded-md border border-transparent p-6 text-left hover:rounded-md ltr:mr-4 ltr:last:mr-0 rtl:ml-4 rtl:last:ml-0 lg:w-1/3"
className="hover:bg-subtle bg-muted w-full self-stretch rounded-md border border-transparent p-6 text-left transition hover:rounded-md ltr:mr-4 ltr:last:mr-0 rtl:ml-4 rtl:last:ml-0 lg:w-1/3"
key={index}
data-testid={embed.type}
onClick={() => {
@@ -254,6 +254,7 @@ export default function CreateEventTypeDialog({
placeholder={t("quick_video_meeting")}
firstRender={firstRender}
setFirstRender={setFirstRender}
maxHeight="200px"
/>
<div className="relative">
@@ -143,7 +143,7 @@ type Props = InputHTMLAttributes<HTMLInputElement> & {
export const FilterCheckboxField = forwardRef<HTMLInputElement, Props>(({ label, icon, ...rest }, ref) => {
return (
<div className="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 hover:cursor-pointer">
<div className="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 transition hover:cursor-pointer">
<label className="flex w-full max-w-full items-center justify-between hover:cursor-pointer">
<div className="flex items-center truncate">
{icon && (
@@ -223,6 +223,7 @@ export const Components: Record<FieldType, Component> = {
props.setValue(val);
}}
{...props}
disabled={props.readOnly}
/>
);
},
@@ -364,9 +365,10 @@ export const Components: Record<FieldType, Component> = {
},
radio: {
propsType: propsTypes.radio,
factory: ({ setValue, name, value, options }) => {
factory: ({ setValue, name, value, options, readOnly }) => {
return (
<Group
disabled={readOnly}
value={value}
onValueChange={(e) => {
setValue(e);
@@ -441,7 +443,7 @@ export const Components: Record<FieldType, Component> = {
type="radio"
disabled={readOnly}
name={name}
className="bg-default after:bg-default border-emphasis focus:ring-brand-default hover:bg-subtle hover:after:bg-subtle dark:checked:after:bg-brand-accent flex h-4 w-4 cursor-pointer items-center justify-center text-[--cal-brand] after:h-[6px] after:w-[6px] after:rounded-full after:content-[''] after:hover:block focus:ring-2 focus:ring-offset-0 ltr:mr-2 rtl:ml-2 dark:checked:hover:text-[--cal-brand]"
className="bg-default after:bg-default border-emphasis focus:ring-brand-default hover:bg-subtle hover:after:bg-subtle dark:checked:after:bg-brand-accent flex h-4 w-4 cursor-pointer items-center justify-center text-[--cal-brand] transition after:h-[6px] after:w-[6px] after:rounded-full after:content-[''] after:hover:block focus:ring-2 focus:ring-offset-0 ltr:mr-2 rtl:ml-2 dark:checked:hover:text-[--cal-brand]"
value={option.value}
onChange={(e) => {
setValue({
+13 -3
View File
@@ -11,6 +11,7 @@ import {
Badge,
BooleanToggleGroupField,
Button,
CheckboxField,
Dialog,
DialogClose,
DialogContent,
@@ -179,7 +180,7 @@ export const FormBuilder = function FormBuilder({
<li
key={field.name}
data-testid={`field-${field.name}`}
className="hover:bg-muted group relative flex items-center justify-between p-4 ">
className="hover:bg-muted group relative flex items-center justify-between p-4 transition">
{!disabled && (
<>
{index >= 1 && (
@@ -237,7 +238,7 @@ export const FormBuilder = function FormBuilder({
onCheckedChange={(checked) => {
update(index, { ...field, hidden: !checked });
}}
classNames={{ container: "p-2 hover:bg-subtle rounded" }}
classNames={{ container: "p-2 hover:bg-subtle rounded transition" }}
tooltip={t("show_on_booking_page")}
/>
)}
@@ -491,6 +492,10 @@ function FieldEditDialog({
}
label={t("identifier")}
/>
<CheckboxField
description={t("disable_input_if_prefilled")}
{...fieldForm.register("disableOnPrefill", { setValueAs: Boolean })}
/>
<InputField
{...fieldForm.register("label")}
// System fields have a defaultLabel, so there a label is not required
@@ -709,7 +714,7 @@ function VariantFields({
onCheckedChange={(checked) => {
fieldForm.setValue("variant", checked ? otherVariant : defaultVariant);
}}
classNames={{ container: "p-2 mt-2 sm:hover:bg-muted rounded" }}
classNames={{ container: "p-2 mt-2 sm:hover:bg-muted rounded transition" }}
tooltip={t("Toggle Variant")}
/>
) : (
@@ -727,6 +732,11 @@ function VariantFields({
label={t("identifier")}
/>
<CheckboxField
description={t("disable_input_if_prefilled")}
{...fieldForm.register("disableOnPrefill", { setValueAs: Boolean })}
/>
<ul
className={classNames(
!isSimpleVariant ? "border-subtle divide-subtle mt-2 divide-y rounded-md border" : ""
@@ -11,6 +11,10 @@ import { Components, isValidValueProp } from "./Components";
import { fieldTypesConfigMap } from "./fieldTypes";
import { fieldsThatSupportLabelAsSafeHtml } from "./fieldsThatSupportLabelAsSafeHtml";
import type { fieldsSchema } from "./schema";
import {
useShouldBeDisabledDueToPrefill,
getFieldNameFromErrorMessage,
} from "./useShouldBeDisabledDueToPrefill";
import { getTranslatedConfig as getTranslatedVariantsConfig } from "./utils/variantsConfig";
type RhfForm = {
@@ -59,6 +63,7 @@ export const FormBuilderField = ({
t
);
const shouldBeDisabled = useShouldBeDisabledDueToPrefill(field);
return (
<div data-fob-field-name={field.name} className={classNames(className, hidden ? "hidden" : "")}>
<Controller
@@ -71,7 +76,7 @@ export const FormBuilderField = ({
<ComponentForField
field={{ ...field, label, placeholder, hidden }}
value={value}
readOnly={readOnly}
readOnly={readOnly || shouldBeDisabled}
setValue={(val: unknown) => {
onChange(val);
}}
@@ -84,7 +89,7 @@ export const FormBuilderField = ({
render={({ message }: { message: string | undefined }) => {
message = message || "";
// If the error comes due to parsing the `responses` object(which can have error for any field), we need to identify the field that has the error from the message
const name = message.replace(/\{([^}]+)\}.*/, "$1");
const name = getFieldNameFromErrorMessage(message);
const isResponsesErrorForThisField = name === field.name;
// If the error comes for the specific property of responses(Possible for system fields), then also we would go ahead and show the error
if (!isResponsesErrorForThisField && !error) {
@@ -0,0 +1,77 @@
import { TooltipProvider } from "@radix-ui/react-tooltip";
import { render } from "@testing-library/react";
import React from "react";
import type { ReactNode } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { vi } from "vitest";
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import { mockProps, questionUtils, setMockIntersectionObserver, setMockMatchMedia } from "./testUtils";
vi.mock("@formkit/auto-animate/react", () => ({
useAutoAnimate: () => [null],
}));
const renderComponent = () => {
const Wrapper = ({ children }: { children: ReactNode }) => {
const methods = useForm();
return (
<TooltipProvider>
<FormProvider {...methods}>{children}</FormProvider>
</TooltipProvider>
);
};
return render(<FormBuilder {...mockProps} />, { wrapper: Wrapper });
};
const questionTypes = [
{ questionType: "email", label: "Email Field" },
{ questionType: "phone", label: "Phone Field" },
{ questionType: "address", label: "Address Field" },
{ questionType: "text", label: "Short Text Field" },
{ questionType: "number", label: "Number Field" },
{ questionType: "textarea", label: "Long Text Field" },
{ questionType: "select", label: "Select Field" },
{ questionType: "multiselect", label: "MultiSelect Field" },
{ questionType: "multiemail", label: "Multiple Emails Field" },
{ questionType: "checkbox", label: "CheckBox Group Field" },
{ questionType: "radio", label: "Radio Group Field" },
{ questionType: "boolean", label: "Checkbox Field" },
];
describe("Form Builder - Add and Edit", () => {
beforeAll(() => {
setMockMatchMedia();
setMockIntersectionObserver();
});
beforeEach(() => {
renderComponent();
});
for (const { questionType, label } of questionTypes) {
it(`should successfully create or edit a ${label} question type in the form builder, with the option to disable the field on prefill`, async () => {
const defaultIdentifier = `${questionType}-id`;
const newIdentifier = `${defaultIdentifier}-edited`;
// Add a question
await questionUtils.addQuestion({
questionType,
identifier: defaultIdentifier,
label,
disableOnPrefill: true,
});
questionUtils.expectedValueForDisablePreFillCheckox(true);
// Edit the question
await questionUtils.editQuestion({
identifier: newIdentifier,
existingQuestionId: defaultIdentifier,
disableOnPrefill: false,
});
questionUtils.expectedValueForDisablePreFillCheckox(false);
});
}
});
@@ -0,0 +1,184 @@
import { fireEvent, waitFor, screen } from "@testing-library/react";
import { vi } from "vitest";
import type { z } from "zod";
import type { fieldsSchema } from "@calcom/features/form-builder/schema";
type RhfForm = {
fields: z.infer<typeof fieldsSchema>;
};
type RhfFormFields = RhfForm["fields"];
export type RhfFormField = RhfFormFields[number];
export type FieldType = RhfFormField["type"];
export interface QuestionProps {
questionType: string;
identifier: string;
label: string;
disableOnPrefill?: boolean;
}
export interface FormBuilderProps {
formProp: string;
title: string;
description: string;
addFieldLabel: string;
disabled: boolean;
LockedIcon: false | JSX.Element;
dataStore: {
options: Record<string, { label: string; value: string; inputPlaceholder?: string }[]>;
};
disableOnPrefill?: boolean;
}
export interface FormBuildFieldProps {
field: Omit<RhfFormField, "editable" | "label"> & {
// Label is optional because radioInput doesn't have a label
label?: string;
value: string;
};
readOnly: boolean;
}
export const mockProps: FormBuilderProps = {
formProp: "formProp",
title: "FormBuilder Title",
description: "FormBuilder Description",
addFieldLabel: "Add Field",
disabled: false,
LockedIcon: false,
dataStore: { options: {} },
disableOnPrefill: false,
};
export const mockFieldProps: FormBuildFieldProps = {
field: {
type: "text",
name: "test-text-n",
variant: "text",
placeholder: "text-test-p",
required: false,
label: "text-test-l",
disableOnPrefill: false,
value: "",
},
readOnly: false,
};
export const setMockMatchMedia = () => {
Object.defineProperty(window, "matchMedia", {
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
};
export const setMockIntersectionObserver = () => {
Object.defineProperty(window, "IntersectionObserver", {
value: vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
takeRecords: vi.fn(() => []),
})),
});
};
export const clickDisablePrefillBasedOnUserInput = (disableOnPrefill?: boolean) => {
if (typeof disableOnPrefill === "boolean") {
// Get the checkbox element
const checkbox = getDisablePrefillCheckbox(disableOnPrefill) as HTMLInputElement;
// Determine if the checkbox's current state differs from the `disableOnPrefill` prop
const isChecked = checkbox ? checkbox.checked : false;
// Click the checkbox only if its current state differs from the `disableOnPrefill` value
if (isChecked !== disableOnPrefill) {
fireEvent.click(checkbox);
}
}
};
export const getDisablePrefillCheckbox = (disableOnPrefill?: boolean) => {
if (typeof disableOnPrefill === "boolean") {
const checkbox = screen.getByRole("checkbox", { name: /disable_input_if_prefilled/i });
return checkbox;
}
};
export const checkForDisablePreFillCheckox = (disableOnPrefill?: boolean) => {
const checkbox = getDisablePrefillCheckbox(disableOnPrefill);
expect(checkbox).toBeInTheDocument();
};
export const questionUtils = {
addQuestion: async (props: QuestionProps) => {
fireEvent.click(screen.getByTestId("add-field"));
checkForDisablePreFillCheckox(props?.disableOnPrefill);
fireEvent.keyDown(screen.getByTestId("test-field-type"), { key: "ArrowDown", code: "ArrowDown" });
fireEvent.click(screen.getByTestId(`select-option-${props.questionType}`));
fireEvent.change(screen.getAllByRole("textbox")[0], { target: { value: props.identifier } });
fireEvent.change(screen.getAllByRole("textbox")[1], { target: { value: props.label } });
clickDisablePrefillBasedOnUserInput(props?.disableOnPrefill);
fireEvent.click(screen.getByTestId("field-add-save"));
await waitFor(() => {
expect(screen.queryByTestId(`field-${props.identifier}`)).toBeInTheDocument();
});
},
editQuestion: async (props: {
identifier: string;
existingQuestionId: string;
disableOnPrefill?: boolean;
}) => {
fireEvent.click(screen.getByTestId("edit-field-action"));
checkForDisablePreFillCheckox(props?.disableOnPrefill);
fireEvent.change(screen.getAllByRole("textbox")[0], { target: { value: props.identifier } });
clickDisablePrefillBasedOnUserInput(props?.disableOnPrefill);
fireEvent.click(screen.getByTestId("field-add-save"));
await waitFor(() => {
expect(screen.queryByTestId(`field-${props.identifier}`)).toBeInTheDocument();
expect(screen.queryByTestId(`field-${props.existingQuestionId}`)).not.toBeInTheDocument();
});
},
deleteQuestion: async (existingQuestionId: string) => {
expect(screen.queryByTestId(`field-${existingQuestionId}`)).toBeInTheDocument();
fireEvent.click(screen.getByTestId("delete-field-action"));
await waitFor(() => {
expect(screen.queryByTestId(`field-${existingQuestionId}`)).not.toBeInTheDocument();
});
},
hideQuestion: async () => {
fireEvent.click(screen.getByTestId("toggle-field"));
await waitFor(() => {
expect(screen.queryByText(/hidden/i)).toBeInTheDocument();
});
},
requiredAndOptionalQuestion: async () => {
expect(screen.queryByTestId("required")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("edit-field-action"));
fireEvent.click(screen.getAllByRole("radio")[1]);
fireEvent.click(screen.getByTestId("field-add-save"));
await waitFor(() => {
expect(screen.getByTestId("optional")).toBeInTheDocument();
});
},
expectedValueForDisablePreFillCheckox: async (expectedValue: boolean) => {
fireEvent.click(screen.getByTestId("edit-field-action"));
const checkbox = getDisablePrefillCheckbox(expectedValue) as HTMLInputElement;
await expect(checkbox?.checked).toBe(expectedValue);
},
};

Some files were not shown because too many files have changed in this diff Show More