Merge branch 'main' into feat/organizations

This commit is contained in:
Leo Giovanetti 2023-06-05 11:08:43 -03:00
commit 5c5b936741
44 changed files with 566 additions and 173 deletions

View File

@ -8,8 +8,6 @@ Fixes # (issue)
Loom Video: https://www.loom.com/
-->
**Environment**: Staging(main branch) / Production
## Type of change
<!-- Please delete bullets that are not relevant. -->
@ -27,13 +25,14 @@ Fixes # (issue)
- [ ] Test A
- [ ] Test B
## Mandatory Tasks
- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
## Checklist
<!-- Please remove all the irrelevant bullets to your PR -->
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- My code doesn't follow the style guidelines of this project
- I haven't performed a self-review of my own code and corrected any misspellings
- I haven't commented my code, particularly in hard-to-understand areas
- I haven't checked if my PR needs changes to the documentation
- I haven't checked if my changes generate no new warnings

View File

@ -26,7 +26,6 @@ import { defaultResponder } from "@calcom/lib/server";
* required:
* - eventTypeId
* - start
* - end
* - name
* - email
* - timeZone

View File

@ -15,7 +15,7 @@ interface IConnectCalendarsProps {
const ConnectedCalendars = (props: IConnectCalendarsProps) => {
const { nextStep } = props;
const queryConnectedCalendars = trpc.viewer.connectedCalendars.useQuery();
const queryConnectedCalendars = trpc.viewer.connectedCalendars.useQuery({ onboarding: true });
const { t } = useLocale();
const queryIntegrations = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });

View File

@ -79,7 +79,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
const ActionButtons = () => {
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
<div className="ms-2 me-2 mt-px flex flex-row space-x-2">
<div className="ms-2 me-2 flex flex-row space-x-2">
<Button
type="button"
onClick={() => setOpenDialogSaveUsername(true)}
@ -142,7 +142,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
</div>
)}
</div>
<div className="mt-5 hidden md:inline">
<div className="mt-7 hidden md:inline">
<ActionButtons />
</div>
</div>

View File

@ -26,12 +26,6 @@ export const CheckedSelect = ({
return (
<>
<Select
styles={{
option: (styles, { isDisabled }) => ({
...styles,
backgroundColor: isDisabled ? "#F5F5F5" : "inherit",
}),
}}
name={props.name}
placeholder={props.placeholder || t("select")}
isSearchable={false}

View File

@ -5,7 +5,25 @@ export default function useRouterQuery<T extends string>(name: T) {
const existingQueryParams = router.asPath.split("?")[1];
const urlParams = new URLSearchParams(existingQueryParams);
const query = Object.fromEntries(urlParams);
const query: Record<string, string | string[]> = {};
// Following error is thrown by Typescript:
// 'Type 'URLSearchParams' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher'
// We should change the target to higher ES2019 atleast maybe
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
for (const [key, value] of urlParams) {
if (!query[key]) {
query[key] = value;
} else {
let queryValue = query[key];
if (queryValue instanceof Array) {
queryValue.push(value);
} else {
queryValue = query[key] = [queryValue];
queryValue.push(value);
}
}
}
const setQuery = (newValue: string | number | null | undefined) => {
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });

View File

@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.9.5.1",
"version": "2.9.6",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",

View File

@ -3,15 +3,13 @@
* caching system that NextJS uses SSG pages.
* TODO: Redirect to user profile on browser
*/
import type { GetStaticPaths, GetStaticProps } from "next";
import type { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next";
import { z } from "zod";
import { getCachedResults } from "@calcom/core";
import dayjs from "@calcom/dayjs";
import prisma from "@calcom/prisma";
const CalendarCache = () => <div />;
const paramsSchema = z.object({ user: z.string(), month: z.string() });
export const getStaticProps: GetStaticProps<
{ results: Awaited<ReturnType<typeof getCachedResults>> },
@ -29,16 +27,16 @@ export const getStaticProps: GetStaticProps<
selectedCalendars: true,
},
});
const startDate = (
dayjs(month, "YYYY-MM").isSame(dayjs(), "month") ? dayjs.utc() : dayjs.utc(month, "YYYY-MM")
).startOf("day");
const endDate = startDate.endOf("month");
// Subtract 11 hours from the start date to avoid problems in UTC- time zones.
const startDate = dayjs.utc(month, "YYYY-MM").startOf("day").subtract(11, "hours").format();
// Add 14 hours from the start date to avoid problems in UTC+ time zones.
const endDate = dayjs.utc(month, "YYYY-MM").endOf("month").add(14, "hours").format();
try {
const results = userWithCredentials?.credentials
? await getCachedResults(
userWithCredentials?.credentials,
startDate.format(),
endDate.format(),
startDate,
endDate,
userWithCredentials?.selectedCalendars
)
: [];
@ -64,5 +62,8 @@ export const getStaticPaths: GetStaticPaths = () => {
fallback: "blocking",
};
};
type Props = InferGetStaticPropsType<typeof getStaticProps>;
const CalendarCache = (props: Props) =>
process.env.NODE_ENV === "development" ? <pre>{JSON.stringify(props, null, " ")}</pre> : <div />;
export default CalendarCache;

View File

@ -279,9 +279,7 @@ export default function Success(props: SuccessProps) {
darkBrandColor: props.profile.darkBrandColor,
});
const title = t(
`booking_${needsConfirmation ? "booking_submitted" : "confirmed"}${
props.recurringBookings ? "_recurring" : ""
}`
`booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}`
);
const locationToDisplay = getSuccessPageLocationMessage(

View File

@ -117,9 +117,7 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
}
key={router.asPath}>
<Head>
<title>
{APP_NAME} - {t("getting_started")}
</title>
<title>{`${APP_NAME} - ${t("getting_started")}`}</title>
<link rel="icon" href="/favicon.ico" />
</Head>

View File

@ -9,10 +9,10 @@ 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 { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Alert, Button, EmailField, HeadSeo, PasswordField, TextField } from "@calcom/ui";
@ -158,6 +158,8 @@ export default function Signup({ prepopulateFormValues, token }: inferSSRProps<t
}
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const flags = await getFeatureFlagMap(prisma);
const ssr = await ssrInit(ctx);
const token = z.string().optional().parse(ctx.query.token);
@ -168,7 +170,9 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
prepopulateFormValues: undefined,
};
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true") {
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" || flags["disable-signup"]) {
console.log({ flag: flags["disable-signup"] });
return {
notFound: true,
};

View File

@ -1673,6 +1673,7 @@
"booking_questions_title": "Booking questions",
"booking_questions_description": "Customize the questions asked on the booking page",
"add_a_booking_question": "Add a question",
"identifier": "Identifier",
"duplicate_email": "Email is duplicate",
"booking_with_payment_cancelled": "Paying for this event is no longer possible",
"booking_with_payment_cancelled_already_paid": "A refund for this booking payment it's on the way.",
@ -1714,6 +1715,7 @@
"spot_popular_event_types": "Spot popular event types",
"spot_popular_event_types_description": "See which of your event types are receiving the most clicks and bookings",
"no_responses_yet": "No responses yet",
"no_routes_defined": "No routes defined",
"this_will_be_the_placeholder": "This will be the placeholder",
"error_booking_event": "An error occured when booking the event, please refresh the page and try again",
"timeslot_missing_title": "No timeslot selected",

View File

@ -1669,6 +1669,7 @@
"booking_questions_title": "Questions de réservation",
"booking_questions_description": "Personnalisez les questions posées sur la page de réservation.",
"add_a_booking_question": "Ajouter une question",
"identifier": "Identifiant",
"duplicate_email": "L'adresse e-mail existe déjà",
"booking_with_payment_cancelled": "Payer pour cet événement n'est plus possible",
"booking_with_payment_cancelled_already_paid": "Un remboursement pour ce paiement de réservation est en route.",
@ -1710,6 +1711,7 @@
"spot_popular_event_types": "Identifiez les types d'événements populaires",
"spot_popular_event_types_description": "Découvrez lesquels de vos types d'événements reçoivent le plus de clics et de réservations.",
"no_responses_yet": "Aucune réponse pour l'instant",
"no_routes_defined": "Aucune route définie",
"this_will_be_the_placeholder": "Ce sera le texte de substitution",
"error_booking_event": "Une erreur s'est produite lors de la réservation de l'événement, veuillez actualiser la page et réessayer",
"timeslot_missing_title": "Aucun créneau sélectionné",

View File

@ -1,4 +1,5 @@
{
"identity_provider": "ספק זהויות",
"trial_days_left": "נותרו לך $t(day, {\"count\": {{days}} }) לשימוש בגרסת הניסיון של רישוי PRO",
"day_one": "יום {{count}}",
"day_other": "{{count}} ימים",
@ -25,6 +26,8 @@
"rejection_confirmation": "לדחות את ההזמנה",
"manage_this_event": "נהל את האירוע הזה",
"invite_team_member": "הזמן חבר צוות",
"invite_team_individual_segment": "הזמנת אדם אחד",
"invite_team_bulk_segment": "ייבוא בכמות גדולה",
"invite_team_notifcation_badge": "הזמנה",
"your_event_has_been_scheduled": "נקבע מועד לאירוע שלך",
"your_event_has_been_scheduled_recurring": "נקבע מועד לאירוע החוזר שלך",
@ -69,6 +72,8 @@
"event_awaiting_approval_subject": "ממתין לאישור: {{title}} ב- {{date}}",
"event_still_awaiting_approval": "אירוע עדיין ממתין לאישורך",
"booking_submitted_subject": "ההזמנה נשלחה: {{title}} ב- {{date}}",
"download_recording_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": "עודכן מועד האירוע שלך השתנה",
@ -219,11 +224,13 @@
"go_back_login": "חזרה לדף הכניסה",
"error_during_login": "אירעה שגיאה בעת הכניסה. נא לחזור אל מסך הכניסה ולנסות שוב.",
"request_password_reset": "לשלוח בקשת איפוס",
"send_invite": "שליחת הזמנה",
"forgot_password": "שכחת את הסיסמה?",
"forgot": "שכחת?",
"done": "בוצע",
"all_done": "זהו, סיימת!",
"all_apps": "כל האפליקציות",
"available_apps": "אפליקציות זמינות",
"check_email_reset_password": "בדוק/י בתיבת הדוא\"ל שלחנו לך קישור לאיפוס הסיסמה.",
"finish": "סיום",
"few_sentences_about_yourself": "מספר משפטים אודותיך. המידע הזה יופיע בדף ה-URL האישי שלך.",
@ -233,6 +240,8 @@
"set_availability": "ציין את הזמינות שלך",
"continue_without_calendar": "להמשיך בלי לוח שנה",
"connect_your_calendar": "קשר את לוח השנה שלך",
"connect_your_video_app": "חיבור אפליקציות הווידאו שלך",
"connect_your_video_app_instructions": "חבר/י את אפליקציות הווידאו שלך כדי להשתמש בהן עבור סוגי האירועים שלך.",
"connect_your_calendar_instructions": "קשר את לוח השנה שלך לצפייה בזמנים עמוסים ובאירועים חדשים באופן אוטומטי ברגע שנקבע מועד עבורם.",
"set_up_later": "להגדיר מאוחר יותר",
"current_time": "השעה הנוכחית",
@ -362,6 +371,7 @@
"create_webhook": "יצירת Webhook",
"booking_cancelled": "ההזמנה בוטלה",
"booking_rescheduled": "מועד ההזמנה השתנה",
"recording_ready": "הקישור להורדת ההקלטה מוכן",
"booking_created": "ההזמנה נוצרה",
"meeting_ended": "הפגישה הסתיימה",
"form_submitted": "הטופס נשלח",
@ -403,6 +413,7 @@
"add_time_availability": "הוספת חלון זמן חדש",
"add_an_extra_layer_of_security": "כדאי להוסיף רמת אבטחה נוספת לחשבון, למקרה שהסיסמה שלך תיגנב.",
"2fa": "אימות דו-גורמי",
"2fa_disabled": "ניתן להפעיל אימות דו-גורמי רק לאימות בדוא\"ל ובאמצעות סיסמה",
"enable_2fa": "הפעלת אימות דו-גורמי",
"disable_2fa": "השבתת אימות דו-גורמי",
"disable_2fa_recommendation": "אם עליך להשבית את האימות הדו-גורמי, מומלץ להפעיל אותו שוב בהקדם האפשרי.",
@ -459,6 +470,7 @@
"friday": "יום שישי",
"saturday": "יום שבת",
"sunday": "יום ראשון",
"all_booked_today": "כל מה שהוזמן.",
"slots_load_fail": "לא ניתן לטעון את חלונות הזמן הזמינים.",
"additional_guests": "הוספת אורחים",
"your_name": "שמך",
@ -583,6 +595,20 @@
"minutes": "דקות",
"round_robin": "לפי תורות",
"round_robin_description": "פגישות מחזוריות בין חברי צוות מרובים.",
"managed_event": "אירוע מנוהל",
"username_placeholder": "שם משתמש",
"managed_event_description": "יצירה והפצה של סוגי אירועים בכמות גדולה לחברי צוות",
"managed": "מנוהל",
"managed_event_url_clarification": "יש למלא את שדה \"username\" בשמות המשתמשים של החברים שהוקצו",
"assign_to": "הקצאה ל",
"add_members": "הוספת חברים...",
"count_members_one": "חבר {{count}}",
"count_members_other": "{{count}} חברים",
"no_assigned_members": "לא הוקצה אף חבר",
"assigned_to": "הוקצה ל",
"start_assigning_members_above": "התחל/י להקצות חברים למעלה",
"locked_fields_admin_description": "חברים לא יוכלו לערוך את זה",
"locked_fields_member_description": "האפשרות הזו ננעלה על ידי מנהל הצוות",
"url": "כתובת URL",
"hidden": "מוסתר",
"readonly": "לקריאה בלבד",
@ -732,8 +758,17 @@
"new_event_type_to_book_description": "צור סוג אירוע חדש שאנשים יוכלו לקבוע מועדים באמצעותו.",
"length": "משך זמן",
"minimum_booking_notice": "משך הזמן המינימלי לפני ביצוע הזמנה",
"offset_toggle": "הזזת זמני ההתחלה",
"offset_toggle_description": "הזזת חלונות הזמן שמוצגים למזמינים במספר מוגדר של דקות",
"offset_start": "הזזה של",
"offset_start_description": "לדוגמה, הבחירה באפשרות זו תציג למזמינים את חלונות הזמן כ-{{ adjustedTime }} במקום כ-{{ originalTime }}",
"slot_interval": "מרווחי חלונות זמן",
"slot_interval_default": "השתמש במשך האירוע (ברירת מחדל)",
"delete_event_type": "למחוק את סוג האירוע?",
"delete_managed_event_type": "למחוק את סוג האירוע המנוהל?",
"delete_event_type_description": "כל מי ששיתפת איתו את הקישור הזה כבר לא יוכל להזמין באמצעותו.",
"delete_managed_event_type_description": "<ul><li>סוגי האירועים האלה יימחקו גם אצל חברים שהוקצו לסוגי אירועים אלה.</li><li>כל מי שהם שיתפו איתו את הקישור שלהם כבר לא יוכל להזמין באמצעותו.</li></ul>",
"confirm_delete_event_type": "כן, למחוק",
"delete_account": "מחיקת חשבון",
"confirm_delete_account": "כן, למחוק את החשבון",
"delete_account_confirmation_message": "בטוח שברצונך למחוק את חשבון {{appName}} שלך? כל מי ששיתפת איתו את הקישור לחשבון לא יוכל יותר להזמין באמצעותו, וכל ההעדפות ששמרת יאבדו.",
@ -879,6 +914,7 @@
"duplicate": "כפילות",
"offer_seats": "הצעת מקומות",
"offer_seats_description": "הצעת מקומות להזמנות (אפשרות זו משביתה באופן אוטומטי אישורי הזמנות ואורחים).",
"seats_available_one": "מקום זמין",
"seats_available_other": "מקומות זמינים",
"number_of_seats": "מספר המקומות לכל הזמנה",
"enter_number_of_seats": "יש להזין את מספר המקומות",
@ -1013,6 +1049,7 @@
"event_cancelled_trigger": "כאשר אירוע מבוטל",
"new_event_trigger": "כאשר אירוע חדש מוזמן",
"email_host_action": "שלח מייל למארח",
"email_attendee_action": "שליחת דוא\"ל למשתתפים",
"sms_attendee_action": "שלח SMS למשתתף",
"sms_number_action": "שלח SMS למספר מסוים",
"workflows": "תהליכים",
@ -1123,11 +1160,13 @@
"current_username": "שם משתמש נוכחי",
"example_1": "דוגמה 1",
"example_2": "דוגמה 2",
"booking_question_identifier": "מזהה שאלת הזמנה",
"company_size": "גודל החברה",
"what_help_needed": "במה דרושה לך עזרה?",
"variable_format": "הפורמט של המשתנה",
"webhook_subscriber_url_reserved": "כתובת ה-URL של מנוי Webhook כבר מוגדרת",
"custom_input_as_variable_info": "התעלמות מכל התווים המיוחדים של תווית הקלט הנוספת (שימוש באותיות ובספרות בלבד), שימוש באותיות רישיות עבור כל האותיות והחלפת רווחים במקפים תחתונים.",
"using_booking_questions_as_variables": "איך משתמשים בשאלות הזמנה בתור משתנים?",
"download_desktop_app": "הורדת האפליקציה למחשבים שולחניים",
"set_ping_link": "הגדרת קישור Ping",
"rate_limit_exceeded": "חריגה מהגבלת קצב",
@ -1163,6 +1202,7 @@
"create_workflow": "יצירת תהליך עבודה",
"do_this": "עשה את זה",
"turn_off": "כבה",
"turn_on": "הפעלה",
"settings_updated_successfully": "עדכון ההגדרות בוצע בהצלחה",
"error_updating_settings": "שגיאה בעדכון ההגדרות",
"personal_cal_url": "כתובת ה-URL האישית שלי של {{appName}}",
@ -1173,6 +1213,7 @@
"start_of_week": "תחילת השבוע",
"recordings_title": "הקלטות",
"recording": "הקלטה",
"happy_scheduling": "תזמון נעים!",
"select_calendars": "בחר את לוחות השנה שבהם ברצונך לבדוק אם יש התנגשויות, כדי למנוע כפל הזמנות.",
"check_for_conflicts": "בדיקת התנגשויות",
"view_recordings": "צפייה בהקלטות",
@ -1210,6 +1251,7 @@
"impersonation": "התחזות",
"impersonation_description": "הגדרות לניהול התחזות למשתמשים",
"users": "משתמשים",
"user": "משתמש",
"profile_description": "ניהול הגדרות עבור פרופיל {{appName}} שלך",
"users_description": "כאן אפשר למצוא רשימה של כל המשתמשים",
"users_listing": "רשימת משתמשים",
@ -1217,6 +1259,7 @@
"calendars_description": "הגדרת האינטראקציה בין סוגי האירועים שלך לבין לוחות השנה שלך",
"appearance_description": "ניהול ההגדרות של מראה ההזמנות שלך",
"conferencing_description": "הוסף/הוסיפי את האפליקציות האהובות עליך/ייך לשיחות ועידה לשימוש בפגישות",
"add_conferencing_app": "הוספת אפליקציה לשיחות ועידה",
"password_description": "נהל/י את ההגדרות של סיסמאות החשבון שלך",
"2fa_description": "נהל/י את ההגדרות של סיסמאות החשבון שלך",
"we_just_need_basic_info": "אנחנו זקוקים רק לפרטים בסיסיים כדי להגדיר את הפרופיל שלך.",
@ -1250,6 +1293,8 @@
"download_responses": "הורד תגובות",
"download_responses_description": "הורד את כל התגובות לטופס שלך בפורמט CSV.",
"download": "הורדה",
"download_recording": "הורדת ההקלטה",
"recording_from_your_recent_call": "הקלטה של שיחה שערכת לאחרונה ב-Cal.com מוכנה להורדה",
"create_your_first_form": "צור/צרי את הטופס הראשון שלך",
"create_your_first_form_description": "באמצעות טפסי ניתוב ניתן לשאול שאלות סיווג ולנתב אל האדם או אל סוג האירוע המתאימים.",
"create_your_first_webhook": "יצירת ה-Webhook הראשון שלך",
@ -1286,6 +1331,15 @@
"exchange_authentication_standard": "אימות בסיסי",
"exchange_authentication_ntlm": "אימות NTLM",
"exchange_compression": "דחיסת GZip",
"exchange_version": "גרסת Exchange",
"exchange_version_2007_SP1": "2007 SP1",
"exchange_version_2010": "2010",
"exchange_version_2010_SP1": "2010 SP1",
"exchange_version_2010_SP2": "2010 SP2",
"exchange_version_2013": "2013",
"exchange_version_2013_SP1": "2013 SP1",
"exchange_version_2015": "2015",
"exchange_version_2016": "2016",
"routing_forms_description": "צור טפסים להפניית משתתפים ליעדים הנכונים",
"routing_forms_send_email_owner": "שליחת דוא\"ל לבעלים",
"routing_forms_send_email_owner_description": "דוא\"ל נשלח לבעלים לאחר שליחת הטופס",
@ -1340,6 +1394,7 @@
"add_limit": "הוספת הגבלה",
"team_name_required": "נדרש שם צוות",
"show_attendees": "שיתוף האורחים בפרטי המשתתפים",
"how_booking_questions_as_variables": "איך משתמשים בשאלות הזמנה בתור משתנים?",
"format": "פורמט",
"uppercase_for_letters": "שימוש באותיות רישיות עבור כל האותיות",
"replace_whitespaces_underscores": "החלפת רווחים במקפים תחתונים",
@ -1354,6 +1409,7 @@
"billing_help_title": "צריך/ה משהו נוסף?",
"billing_help_description": "אם דרוש לך סיוע נוסף בענייני חיוב, צוות התמיכה שלנו כאן כדי לעזור.",
"billing_help_cta": "פנייה לתמיכה",
"ignore_special_characters_booking_questions": "להתעלם מתווים מיוחדים במזהה שאלת ההזמנה, להשתמש באותיות ובספרות בלבד",
"retry": "ניסיון נוסף",
"fetching_calendars_error": "אירעה בעיה בטעינת לוחות השנה שלך. <1>נסה/י שוב</1> או פנה/י למחלקת תמיכת הלקוחות.",
"calendar_connection_fail": "החיבור ללוח השנה נכשל",
@ -1436,11 +1492,17 @@
"fixed_round_robin": "סבב קבוע",
"add_one_fixed_attendee": "הוסף/י משתתף/ת קבוע/ה אחד/ת ועבור/י בסבב בין מספר משתתפים.",
"calcom_is_better_with_team": "Cal.com טוב יותר לשימוש עם צוותים",
"the_calcom_team": "הצוות של Cal.com",
"add_your_team_members": "הוסף/י את חברי הצוות לסוגי האירועים שלך. השתמש/י בתזמון שיתופי כדי לכלול את כולם או מצא/י את האדם הכי מתאים בעזרת תזמון בסבב.",
"booking_limit_reached": "הגעת להגבלת ההזמנות עבור סוג אירוע זה",
"duration_limit_reached": "מגבלת הזמן לארוע זה עבר",
"admin_has_disabled": "מנהל/ת מערכת השבית/ה את {{appName}}",
"disabled_app_affects_event_type": "מנהל/ת מערכת השבית/ה את {{appName}}, ויש לכך השפעה על האירוע שלך מסוג {{eventType}}",
"event_replaced_notice": "מנהל/ת מערכת החליף/ה אחד מסוגי האירועים שלך",
"email_subject_slug_replacement": "מנהל/ת צוות החליף/ה את האירוע שלך /{{slug}}",
"email_body_slug_replacement_notice": "מנהל/ת בצוות <strong>{{teamName}}</strong> החליף/ה את סוג האירוע שלך, <strong>/{{slug}}</strong>, בסוג של אירוע מנוהל שתהיה לו/ה אפשרות לשלוט בו.",
"email_body_slug_replacement_info": "הקישור שלך ימשיך לעבוד, אבל ייתכן שחלק מההגדרות עבורו השתנו. ניתן לבדוק אותו בסוגי האירועים.",
"email_body_slug_replacement_suggestion": "אם יש לך שאלות לגבי סוג האירוע, פנה/י אל מנהל/ת המערכת.<br /><br />שיהיה תזמון נעים, <br />הצוות של Cal.com",
"disable_payment_app": "מנהל/ת המערכת השבית/ה את {{appName}}, ויש לכך השפעה על סוג האירוע שלך בשם {{title}}. המשתתפים עדיין יוכלו להזמין אירוע מסוג זה, אבל לא תוצג להם בקשה לביצוע תשלום. ניתן להסתיר את סוג האירוע הזה על מנת למנוע מצב זה עד שמנהל/ת המערכת יפעיל/תפעיל שוב את שיטת התשלום.",
"payment_disabled_still_able_to_book": "המשתתפים עדיין יוכלו להזמין אירוע מסוג זה, אבל לא תוצג להם בקשה לביצוע תשלום. ניתן להסתיר את סוג האירוע הזה על מנת למנוע מצב זה עד שמנהל/ת המערכת יפעיל/תפעיל שוב את שיטת התשלום.",
"app_disabled_with_event_type": "מנהל/ת המערכת השבית/ה את {{appName}}, ויש לכך השפעה על האירוע שלך מסוג {{title}}.",
@ -1489,6 +1551,7 @@
"date_overrides_update_btn": "עדכון מעקף",
"event_type_duplicate_copy_text": "{{slug}}-עותק",
"set_as_default": "להגדיר כברירת מחדל",
"hide_eventtype_details": "הסתרת פרטי סוג האירוע",
"show_navigation": "הצגת הניווט",
"hide_navigation": "הסתרת הניווט",
"verification_code_sent": "הקוד לאימות נשלח",
@ -1502,6 +1565,7 @@
"create_your_first_team_webhook_description": "צור/צרי את ה-Webhook הראשון שלך עבור סוג זה של אירוע צוות",
"create_webhook_team_event_type": "צור/צרי Webhook עבור סוג זה של אירוע צוות",
"disable_success_page": "השבתת דף 'הפעולה הצליחה' (עובד רק אם יש לך כתובת URL להפניה אוטומטית)",
"invalid_admin_password": "את/ה מנהל/ת מערכת, אבל הסיסמה שלך לא כוללת את האורך המינימלי של 15 תווים או שלא הגדרת עדיין אימות דו-גורמי",
"change_password_admin": "שנה/י את הסיסמה כדי לקבל גישה של מנהל/ת מערכת",
"username_already_taken": "שם המשתמש הזה כבר תפוס",
"assignment": "הקצאה",
@ -1534,6 +1598,7 @@
"ee_enterprise_license": "\"ee/\" רישיון Enterprise",
"enterprise_booking_fee": "החל מ- {{enterprise_booking_fee}}/לחודש",
"enterprise_license_includes": "הכל לשימוש מסחרי",
"no_need_to_keep_your_code_open_source": "אין צורך לשמור את הקוד שלך כקוד פתוח",
"repackage_rebrand_resell": "אריזה מחדש, מיתוג מחדש ומכירה בקלות",
"a_vast_suite_of_enterprise_features": "מגוון יכולות Enterprise עצום",
"free_license_fee": "$0.00/לחודש",
@ -1569,6 +1634,7 @@
"email_user_cta": "צפה בהזמנה",
"email_no_user_invite_heading": "הוזמנת להצטרף לצוות ב- {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} הזמין אותך להצטרף לצוות שלו ב- {{appName}}. {{appName}} הינה מתזמן זימונים שמאפשר לך ולצוות שלך לזמן פגישות בלי כל הפינג פונג במיילים.",
"email_user_invite_subheading": "{{invitedBy}} הזמין/ה אותך להצטרף לצוות שלו/ה בשם '{{teamName}}' באפליקציה {{appName}}. אפליקציית {{appName}} היא כלי לקביעת מועדים לאירועים שמאפשר לך ולצוות שלך לתזמן פגישות בלי כל הפינג פונג במיילים.",
"email_no_user_invite_steps_intro": "אנחנו נדריך אותך במספר צעדים קצרים ואתה תהנה תזמון נטול מתח עם הצוות שלך כהרף עין.",
"email_no_user_step_one": "בחר שם משתמש",
"email_no_user_step_two": "קשר את לוח השנה שלך",
@ -1595,6 +1661,7 @@
"default_app_link_title": "צור קישור אפליקציה ברירת מחדל",
"default_app_link_description": "הגדרת קישור אפליקציה ברירת מחדל מאפשר לכל הארועים החדשים להשתמש בקישור שהגדרת.",
"change_default_conferencing_app": "להגדיר כברירת מחדל",
"organizer_default_conferencing_app": "אפליקציית ברירת המחדל של המארגן/ת",
"under_maintenance": "אינו זמין עקב תחזוקה",
"under_maintenance_description": "צוות {{appName}} מבצע עבודות תחזוקה שתוכננו מראש. אם יש לך שאלות, נא צור קשר עם התמיכה.",
"event_type_seats": "{{numberOfSeats}} מושבים",
@ -1642,6 +1709,13 @@
"spot_popular_event_types_description": "צפה איזה ארועים מקבלים הכי הרבה גישות ותזמונים",
"no_responses_yet": "אין עדיין תגובות",
"this_will_be_the_placeholder": "זה יהיה הממלא מקום",
"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}}",
@ -1651,12 +1725,27 @@
"can_you_try_again": "אתה יכול לנסות שוב עם שעה אחרת?",
"verify": "אמת",
"timezone_variable": "אזור זמן",
"event_end_time_variable": "שעת סיום האירוע",
"event_end_time_info": "שעת סיום האירוע",
"cancel_url_variable": "ה-URL לביטול",
"cancel_url_info": "כתובת ה-URL לביטול ההזמנה",
"reschedule_url_variable": "ה-URL לתזמון מחדש",
"reschedule_url_info": "כתובת ה-URL לקביעת מועד חדש להזמנה",
"invalid_event_name_variables": "יש משתנה שגוי בשם הארוע שלך",
"select_all": "בחר הכל",
"default_conferencing_bulk_title": "בצע עדכון אצווה של סוגי הארועים הקיימים",
"members_default_schedule": "לוח הזמנים שמוגדר כברירת מחדל עבור החבר/ה",
"set_by_admin": "הגדרה לפי מנהל/ת הצוות",
"members_default_location": "מיקום ברירת המחדל של החבר/ה",
"requires_at_least_one_schedule": "אתה חייב שיהיה לך לפחות לוח זמנים אחד",
"default_conferencing_bulk_description": "עדכן את המיקומים עבור סוגי הארועים שנבחרו",
"locked_apps_description": "החברים יוכלו לראות את האפליקציות הפעילות, אבל לא יוכלו לערוך הגדרות של האפליקציה",
"app_not_connected": "לא קישרת חשבון {{appName}}.",
"connect_now": "להתחבר עכשיו",
"managed_event_dialog_confirm_button_one": "החלפה ועדכון של חבר/ה {{count}}",
"managed_event_dialog_confirm_button_other": "החלפה ועדכון של {{count}} חברים",
"looking_for_more_analytics": "מחפש עוד מידע אנליטי?",
"looking_for_more_insights": "רצית עוד Insights?",
"add_filter": "הוסף סנן",
"select_user": "בחר משתמש",
"select_event_type": "בחר סוג ארוע",
@ -1672,5 +1761,14 @@
"events_rescheduled": "ארועים שתוזמנו מחדש",
"from_last_period": "מפרק הזמן האחרון",
"from_to_date_period": "מ: {{startDate}} עד: {{endDate}}",
"event_trends": "מגמות ארוע"
"analytics_for_organisation": "Insights",
"subtitle_analytics": "קבל/י מידע נוסף על הפעילות של הצוות שלך",
"event_trends": "מגמות ארוע",
"clear_filters": "ניקוי המסננים",
"acknowledge_booking_no_show_fee": "מובן לי שאם לא אשתתף באירוע הזה, דמי אי-הגעה בסך {{amount, currency}} ינוכו מהכרטיס שלי.",
"card_details": "פרטי כרטיס",
"seats_and_no_show_fee_error": "נכון לעכשיו, אי אפשר להפעיל מקומות ולחייב דמי אי-הגעה",
"complete_your_booking": "יש להשלים את ההזמנה",
"complete_your_booking_subject": "יש להשלים את ההזמנה: {{title}} ב-{{date}}",
"payment_app_commission": "דרישת תשלום ({{paymentFeePercentage}}% + {{fee, currency}} עמלה על העסקה)"
}

View File

@ -26,6 +26,8 @@
"rejection_confirmation": "Boeking weigeren",
"manage_this_event": "Deze gebeurtenis beheren",
"invite_team_member": "Teamlid uitnodigen",
"invite_team_individual_segment": "Individueel uitnodigen",
"invite_team_bulk_segment": "Bulkimport",
"invite_team_notifcation_badge": "Uitg.",
"your_event_has_been_scheduled": "Uw gebeurtenis is gepland",
"your_event_has_been_scheduled_recurring": "Uw terugkerende gebeurtenis is gepland",
@ -222,6 +224,7 @@
"go_back_login": "Ga terug naar de inlogpagina",
"error_during_login": "Er is een fout opgetreden tijdens het inloggen. Ga terug naar de inlogpagina en probeer het opnieuw.",
"request_password_reset": "E-mail voor opnieuw instellen van wachtwoord versturen",
"send_invite": "Uitnodiging versturen",
"forgot_password": "Wachtwoord vergeten?",
"forgot": "Vergeten?",
"done": "Voltooid",
@ -237,6 +240,8 @@
"set_availability": "Beschikbaarheid bepalen",
"continue_without_calendar": "Doorgaan zonder kalender",
"connect_your_calendar": "Koppel uw agenda",
"connect_your_video_app": "Koppel uw videoapps",
"connect_your_video_app_instructions": "Koppel uw videoapps om ze op uw gebeurtenistypes te gebruiken.",
"connect_your_calendar_instructions": "Koppel uw agenda om automatisch te controleren op drukke tijden en nieuwe afspraken wanneer ze worden gepland.",
"set_up_later": "Later instellen",
"current_time": "Huidige tijd",
@ -366,6 +371,7 @@
"create_webhook": "Webhook maken",
"booking_cancelled": "Afspraak Geannuleerd",
"booking_rescheduled": "Afspraak verplaatst",
"recording_ready": "Downloadlink opname gereed",
"booking_created": "Boeking aangemaakt",
"meeting_ended": "Vergadering beëindigd",
"form_submitted": "Formulier verzonden",
@ -464,6 +470,7 @@
"friday": "Vrijdag",
"saturday": "Zaterdag",
"sunday": "Zondag",
"all_booked_today": "Alles geboekt.",
"slots_load_fail": "De beschikbare tijdsperiodes konden niet worden geladen.",
"additional_guests": "Gasten toevoegen",
"your_name": "Uw naam",
@ -751,6 +758,10 @@
"new_event_type_to_book_description": "Maak een nieuw afspraak type voor bezoekers om tijden mee te reserveren.",
"length": "Lengte",
"minimum_booking_notice": "Minimale kennisgevingstermijn",
"offset_toggle": "Verschuiving begintijden",
"offset_toggle_description": "Verschuif de aan boekers weergegeven tijdslots met een opgegeven aantal minuten",
"offset_start": "Verschuiven met",
"offset_start_description": "dit geeft bijvoorbeeld tijdslots aan uw boekers weer op {{ adjustedTime }} in plaats van {{ originalTime }}",
"slot_interval": "Tijdslot intervallen",
"slot_interval_default": "Gebruik evenement lengte (standaard)",
"delete_event_type": "Gebeurtenistype verwijderen?",
@ -903,6 +914,7 @@
"duplicate": "Dupliceren",
"offer_seats": "Zitplaatsen aanbieden",
"offer_seats_description": "Bied beschikbare plaatsen aan voor boekingen. Hiermee worden gastboekingen en aanmeldingsboekingen automatisch uitgeschakeld.",
"seats_available_one": "Plaats beschikbaar",
"seats_available_other": "Beschikbare zitplaatsen",
"number_of_seats": "Aantal zitplaatsen per boeking",
"enter_number_of_seats": "Voer het aantal zitplaatsen in",
@ -1037,6 +1049,7 @@
"event_cancelled_trigger": "wanneer de gebeurtenis is geannuleerd",
"new_event_trigger": "wanneer een nieuwe gebeurtenis is geboekt",
"email_host_action": "e-mail naar organisator versturen",
"email_attendee_action": "e-mail naar deelnemers versturen",
"sms_attendee_action": "sms naar deelnemer versturen",
"sms_number_action": "sms naar een specifiek nummer sturen",
"workflows": "Werkstromen",
@ -1189,6 +1202,7 @@
"create_workflow": "Maak een werkstroom",
"do_this": "Doe dit",
"turn_off": "Uitschakelen",
"turn_on": "Inschakelen",
"settings_updated_successfully": "Instellingen bijgewerkt",
"error_updating_settings": "Fout bij het bijwerken van de instellingen",
"personal_cal_url": "Mijn persoonlijke {{appName}}-URL",
@ -1245,6 +1259,7 @@
"calendars_description": "Configureer hoe uw typen gebeurtenissen samenwerken met uw agenda's",
"appearance_description": "Beheer de instellingen voor uw boekingsweergave",
"conferencing_description": "Beheer uw favoriete apps voor videoconferenties voor uw vergaderingen",
"add_conferencing_app": "Conferentieapp toevoegen",
"password_description": "Beheer de instellingen voor uw accountwachtwoorden",
"2fa_description": "Beheer de instellingen voor uw accountwachtwoorden",
"we_just_need_basic_info": "We hebben alleen wat basisinformatie nodig om uw profiel te kunnen configureren.",
@ -1619,6 +1634,7 @@
"email_user_cta": "Uitnodiging weergeven",
"email_no_user_invite_heading": "U bent uitgenodigd om lid te worden van een team op {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} heeft u uitgenodigd om lid te worden van zijn team op {{appName}}. {{appName}} is de gebeurtenissenplanner die u en uw team in staat stelt vergaderingen te plannen zonder heen en weer te e-mailen.",
"email_user_invite_subheading": "{{invitedBy}} heeft u uitgenodigd om lid te worden van zijn team '{{teamName}}' op {{appName}}. {{appName}} is de gebeurtenissenplanner die u en uw team in staat stelt afspraken te plannen zonder heen en weer te e-mailen.",
"email_no_user_invite_steps_intro": "We doorlopen samen een paar korte stappen en in een mum van tijd kunt u genieten van een stressvrije planning met uw team.",
"email_no_user_step_one": "Kies uw gebruikersnaam",
"email_no_user_step_two": "Koppel uw agenda-account",
@ -1694,6 +1710,15 @@
"spot_popular_event_types_description": "Bekijk welke van uw gebeurtenistypen de meeste kliks en boekingen ontvangen",
"no_responses_yet": "Nog geen reacties",
"this_will_be_the_placeholder": "Dit wordt de plaatshouder",
"error_booking_event": "Er is een fout opgetreden bij het boeken van de gebeurtenis, vernieuw de pagina en probeer het opnieuw",
"timeslot_missing_title": "Geen tijdslot geselecteerd",
"timeslot_missing_description": "Selecteer een tijdslot om de gebeurtenis te boeken.",
"timeslot_missing_cta": "Tijdslot selecteren",
"switch_monthly": "Overschakelen naar maandelijkse weergave",
"switch_weekly": "Overschakelen naar wekelijkse weergave",
"switch_multiday": "Overschakelen naar dagelijkse weergave",
"num_locations": "{{num}} locatieopties",
"select_on_next_step": "Selecteer bij de volgende stap",
"this_meeting_has_not_started_yet": "Deze vergadering is nog niet begonnen",
"this_app_requires_connected_account": "{{appName}} vereist een gekoppeld {{dependencyName}}-account",
"connect_app": "{{dependencyName}} koppelen",
@ -1723,6 +1748,7 @@
"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 teambeheerder",
"app_not_connected": "U hebt geen {{appName}}-account gekoppeld.",
"connect_now": "Nu koppelen",
"managed_event_dialog_confirm_button_one": "{{count}} lid vervangen en informeren",
@ -1780,5 +1806,34 @@
"seats_and_no_show_fee_error": "Kan op dit moment geen plaatsen inschakelen en afwezigheidsvergoeding in rekening brengen",
"complete_your_booking": "Voltooi uw boeking",
"complete_your_booking_subject": "Voltooi uw boeking: {{title}} op {{date}}",
"email_invite_team": "{{email}} is uitgenodigd"
"confirm_your_details": "Bevestig uw gegevens",
"currency_string": "{{amount, currency}}",
"charge_card_dialog_body": "U staat op het punt om de deelnemer {{amount, currency}} in rekening te brengen. Weet u zeker dat u wilt doorgaan?",
"charge_attendee": "Deelnemer {{amount, currency}} in rekening brengen",
"payment_app_commission": "Vereis betaling ({{paymentFeePercentage}}% + {{fee, currency}} commissie per transactie)",
"email_invite_team": "{{email}} is uitgenodigd",
"email_invite_team_bulk": "{{userCount}} gebruikers zijn uitgenodigd",
"error_collecting_card": "Fout bij ophalen kaart",
"image_size_limit_exceed": "De geüploade afbeelding mag niet groter zijn dan 5 MB",
"inline_embed": "Inline insluiten",
"load_inline_content": "Laadt uw gebeurtenistype direct inline met uw andere websitecontent.",
"floating_pop_up_button": "Zwevende pop-upknop",
"floating_button_trigger_modal": "Plaatst een zwevende knop op uw site die een modaal met uw gebeurtenistype activeert.",
"pop_up_element_click": "Pop-up via elementklik",
"open_dialog_with_element_click": "Open uw Cal-dialoogvenster wanneer iemand op een element klikt.",
"need_help_embedding": "Hulp nodig? Bekijk onze handleidingen voor het insluiten van Cal op Wix, Squarespace of WordPress. Bekijk onze veelgestelde vragen of ontdek geavanceerde insluitopties.",
"book_my_cal": "Boek mijn Cal",
"invite_as": "Uitnodigen als",
"form_updated_successfully": "Formulier bijgewerkt.",
"email_not_cal_member_cta": "Neem deel aan uw team",
"disable_attendees_confirmation_emails": "Standaardbevestigings-e-mails voor deelnemers uitschakelen",
"disable_attendees_confirmation_emails_description": "Er is minstens één werkstroom actief voor dit gebeurtenistype die een e-mail naar de deelnemers stuurt wanneer de gebeurtenis geboekt is.",
"disable_host_confirmation_emails": "Standaardbevestigings-e-mails voor organisator uitschakelen",
"disable_host_confirmation_emails_description": "Er is minstens één werkstroom actief voor dit gebeurtenistype die een e-mail naar de organisator stuurt wanneer de gebeurtenis geboekt is.",
"add_an_override": "Voeg een overschrijving toe",
"import_from_google_workspace": "Importeer gebruikers uit Google Workspace",
"connect_google_workspace": "Google Workspace koppelen",
"google_workspace_admin_tooltip": "U moet een Google Workspace-beheerder zijn om deze functie te gebruiken",
"first_event_type_webhook_description": "Maak uw eerste webhook voor dit gebeurtenistype",
"create_for": "Maak voor"
}

View File

@ -1668,6 +1668,7 @@
"booking_questions_title": "预约问题",
"booking_questions_description": "自定义预约页面上提出的问题",
"add_a_booking_question": "添加问题",
"identifier": "标识符",
"duplicate_email": "电子邮件重复",
"booking_with_payment_cancelled": "无法再为此活动付费",
"booking_with_payment_cancelled_already_paid": "此预约的退款正在进行中。",
@ -1835,5 +1836,6 @@
"connect_google_workspace": "连接 Google Workspace",
"google_workspace_admin_tooltip": "您必须是 Workspace 管理员才能使用此功能",
"first_event_type_webhook_description": "为此活动类型创建第一个 Webhook",
"create_for": "创建"
"create_for": "创建",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在上面这里添加新的字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -34,10 +34,18 @@ import {
Tooltip,
VerticalDivider,
} from "@calcom/ui";
import { ExternalLink, Link as LinkIcon, Download, Code, Trash } from "@calcom/ui/components/icon";
import {
ExternalLink,
Link as LinkIcon,
Download,
Code,
Trash,
MessageCircle,
} from "@calcom/ui/components/icon";
import { RoutingPages } from "../lib/RoutingPages";
import { getSerializableForm } from "../lib/getSerializableForm";
import { isFallbackRoute } from "../lib/isFallbackRoute";
import { processRoute } from "../lib/processRoute";
import type { Response, Route, SerializableForm } from "../types/types";
import { FormAction, FormActionsDropdown, FormActionsProvider } from "./FormActions";
@ -371,18 +379,26 @@ function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) {
{t("test_preview")}
</Button>
</div>
{form.routes?.every(isFallbackRoute) && (
<Alert
className="mt-6 !bg-orange-100 font-semibold text-orange-900"
iconClassName="!text-orange-900"
severity="neutral"
title={t("no_routes_defined")}
/>
)}
{!form._count?.responses && (
<>
<Alert
className="mt-6"
className="mt-2 px-4 py-3"
severity="neutral"
title={t("no_responses_yet")}
message={t("responses_collection_waiting_description")}
CustomIcon={MessageCircle}
/>
</>
)}
</div>
<div className="border-subtle w-full rounded-md border p-8">
<div className="border-subtle bg-muted w-full rounded-md border p-8">
<RoutingNavBar appUrl={appUrl} form={form} />
<Page hookForm={hookForm} form={form} appUrl={appUrl} />
</div>

View File

@ -12,11 +12,12 @@ import {
Button,
EmptyScreen,
FormCard,
Label,
SelectField,
TextAreaField,
Skeleton,
TextField,
} from "@calcom/ui";
import { Plus, FileText } from "@calcom/ui/components/icon";
import { Plus, FileText, X } from "@calcom/ui/components/icon";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@ -27,6 +28,7 @@ import SingleForm, {
export { getServerSideProps };
type HookForm = UseFormReturn<RoutingFormWithResponseCount>;
type SelectOption = { placeholder: string; value: string };
export const FieldTypes = [
{
@ -42,11 +44,11 @@ export const FieldTypes = [
value: "textarea",
},
{
label: "Select",
label: "Single Selection",
value: "select",
},
{
label: "MultiSelect",
label: "Multiple Selection",
value: "multiselect",
},
{
@ -85,30 +87,70 @@ function Field({
};
appUrl: string;
}) {
const [identifier, _setIdentifier] = useState(hookForm.getValues(`${hookFieldNamespace}.identifier`));
const { t } = useLocale();
const setUserChangedIdentifier = (val: string) => {
_setIdentifier(val);
// Also, update the form identifier so tha it can be persisted
hookForm.setValue(`${hookFieldNamespace}.identifier`, val);
const [options, setOptions] = useState<SelectOption[]>([
{ placeholder: "< 10", value: "" },
{ placeholder: "10-100", value: "" },
{ placeholder: "100-500", value: "" },
{ placeholder: "> 500", value: "" },
]);
const handleRemoveOptions = (index: number) => {
const updatedOptions = options.filter((_, i) => i !== index);
setOptions(updatedOptions);
updateSelectText(updatedOptions);
};
const label = hookForm.watch(`${hookFieldNamespace}.label`);
const handleAddOptions = () => {
setOptions((prevState) => [
...prevState,
{
placeholder: "New Option",
value: "",
},
]);
};
useEffect(() => {
if (!hookForm.getValues(`${hookFieldNamespace}.identifier`)) {
_setIdentifier(label);
const originalValues = hookForm.getValues(`${hookFieldNamespace}.selectText`);
if (originalValues) {
const values: SelectOption[] = originalValues.split("\n").map((fieldValue) => ({
value: fieldValue,
placeholder: "",
}));
setOptions(values);
}
}, [label, hookFieldNamespace, hookForm]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const router = hookForm.getValues(`${hookFieldNamespace}.router`);
const routerField = hookForm.getValues(`${hookFieldNamespace}.routerField`);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>, optionIndex: number) => {
const updatedOptions = options.map((opt, index) => ({
...opt,
...(index === optionIndex ? { value: e.target.value } : {}),
}));
setOptions(updatedOptions);
updateSelectText(updatedOptions);
};
const updateSelectText = (updatedOptions: SelectOption[]) => {
hookForm.setValue(
`${hookFieldNamespace}.selectText`,
updatedOptions
.filter((opt) => opt.value)
.map((opt) => opt.value)
.join("\n")
);
};
return (
<div
data-testid="field"
className="group mb-4 flex w-full items-center justify-between ltr:mr-2 rtl:ml-2">
className="bg-default group mb-4 flex w-full items-center justify-between ltr:mr-2 rtl:ml-2">
<FormCard
label={label || `Field ${fieldIndex + 1}`}
label="Field"
moveUp={moveUp}
moveDown={moveDown}
badge={
@ -120,6 +162,7 @@ function Field({
<TextField
disabled={!!router}
label="Label"
className="flex-grow"
placeholder={t("this_is_what_your_users_would_see")}
/**
* This is a bit of a hack to make sure that for routerField, label is shown from there.
@ -137,18 +180,16 @@ function Field({
name={`${hookFieldNamespace}.identifier`}
required
placeholder={t("identifies_name_field")}
value={identifier}
defaultValue={routerField?.identifier || routerField?.label}
onChange={(e) => setUserChangedIdentifier(e.target.value)}
/>
</div>
<div className="mb-6 w-full">
<TextField
disabled={!!router}
label={t("placeholder")}
placeholder={t("this_will_be_the_placeholder")}
defaultValue={routerField?.placeholder}
{...hookForm.register(`${hookFieldNamespace}.placeholder`)}
//This change has the same effects that already existed in relation to this field,
// but written in a different way.
// The identifier field will have the same value as the label field until it is changed
defaultValue={
hookForm.watch(`${hookFieldNamespace}.identifier`) ||
hookForm.watch(`${hookFieldNamespace}.label`)
}
onChange={(e) => {
hookForm.setValue(`${hookFieldNamespace}.identifier`, e.target.value);
}}
/>
</div>
<div className="mb-6 w-full ">
@ -160,6 +201,17 @@ function Field({
const defaultValue = FieldTypes.find((fieldType) => fieldType.value === value);
return (
<SelectField
maxMenuHeight={200}
styles={{
singleValue: (baseStyles) => ({
...baseStyles,
fontSize: "14px",
}),
option: (baseStyles) => ({
...baseStyles,
fontSize: "14px",
}),
}}
label="Type"
isDisabled={!!router}
containerClassName="data-testid-field-type"
@ -177,21 +229,48 @@ function Field({
/>
</div>
{["select", "multiselect"].includes(hookForm.watch(`${hookFieldNamespace}.type`)) ? (
<div className="mt-2 block items-center sm:flex">
<div className="w-full">
<TextAreaField
disabled={!!router}
rows={3}
label="Options"
defaultValue={routerField?.selectText}
placeholder={t("add_1_option_per_line")}
{...hookForm.register(`${hookFieldNamespace}.selectText`)}
/>
<div className="mt-2 w-full">
<Skeleton as={Label} loadingClassName="w-16" title={t("Options")}>
{t("options")}
</Skeleton>
{options.map((field, index) => (
<div key={`select-option-${index}`}>
<TextField
disabled={!!router}
containerClassName="[&>*:first-child]:border [&>*:first-child]:border-default hover:[&>*:first-child]:border-gray-400"
className="border-0 focus:ring-0 focus:ring-offset-0"
labelSrOnly
placeholder={field.placeholder.toString()}
value={field.value}
type="text"
addOnClassname="bg-transparent border-0"
onChange={(e) => handleChange(e, index)}
addOnSuffix={
<button
type="button"
onClick={() => handleRemoveOptions(index)}
aria-label={t("remove")}>
<X className="h-4 w-4" />
</button>
}
/>
</div>
))}
<div className={classNames("flex")}>
<Button
data-testid="add-attribute"
className="border-none"
type="button"
StartIcon={Plus}
color="secondary"
onClick={handleAddOptions}>
Add an option
</Button>
</div>
</div>
) : null}
<div className="w-full">
<div className="w-[106px]">
<Controller
name={`${hookFieldNamespace}.required`}
control={hookForm.control}
@ -199,6 +278,7 @@ function Field({
render={({ field: { value, onChange } }) => {
return (
<BooleanToggleGroupField
variant="small"
disabled={!!router}
label={t("required")}
value={value}
@ -256,7 +336,7 @@ const FormEdit = ({
return hookFormFields.length ? (
<div className="flex flex-col-reverse lg:flex-row">
<div className="w-full ltr:mr-2 rtl:ml-2">
<div ref={animationRef} className="flex w-full flex-col">
<div ref={animationRef} className="flex w-full flex-col rounded-md">
{hookFormFields.map((field, key) => {
return (
<Field
@ -298,14 +378,14 @@ const FormEdit = ({
StartIcon={Plus}
color="secondary"
onClick={addField}>
Add Field
Add field
</Button>
</div>
) : null}
</div>
</div>
) : (
<div className="w-full">
<div className="bg-default w-full">
<EmptyScreen
Icon={FileText}
headline="Create your first field"

View File

@ -165,7 +165,7 @@ const Reporter = ({ form }: { form: inferSSRProps<typeof getServerSideProps>["fo
);
return (
<div className="flex flex-col-reverse md:flex-row">
<div className="cal-query-builder w-full ltr:mr-2 rtl:ml-2">
<div className="cal-query-builder bg-default w-full ltr:mr-2 rtl:ml-2">
<Query
{...config}
value={query.state.tree}

View File

@ -420,7 +420,7 @@ const Routes = ({
hookForm.setValue("routes", routesToSave);
return (
<div className="flex flex-col-reverse md:flex-row">
<div className="bg-default border-subtle flex flex-col-reverse rounded-md border p-8 md:flex-row">
<div ref={animationRef} className="w-full ltr:mr-2 rtl:ml-2">
{mainRoutes.map((route, key) => {
return (

View File

@ -82,7 +82,7 @@ function RoutingForm({ form, profile, ...restProps }: Props) {
}, [customPageMessage]);
const responseMutation = trpc.viewer.appRoutingForms.public.response.useMutation({
onSuccess: () => {
onSuccess: async () => {
const decidedActionWithFormResponse = decidedActionWithFormResponseRef.current;
if (!decidedActionWithFormResponse) {
return;
@ -98,7 +98,7 @@ function RoutingForm({ form, profile, ...restProps }: Props) {
if (decidedAction.type === "customPageMessage") {
setCustomPageMessage(decidedAction.value);
} else if (decidedAction.type === "eventTypeRedirectUrl") {
router.push(`/${decidedAction.value}?${allURLSearchParams}`);
await router.push(`/${decidedAction.value}?${allURLSearchParams}`);
} else if (decidedAction.type === "externalRedirectUrl") {
window.parent.location.href = `${decidedAction.value}?${allURLSearchParams}`;
}

View File

@ -136,7 +136,7 @@ test.describe("Routing Forms", () => {
label: "Test Field",
});
const queryString =
"firstField=456&Test Field Number=456&Test Field Select=456&Test Field MultiSelect=456&Test Field MultiSelect=789&Test Field Phone=456&Test Field Email=456@example.com";
"firstField=456&Test Field Number=456&Test Field Single Selection=456&Test Field Multiple Selection=456&Test Field Multiple Selection=789&Test Field Phone=456&Test Field Email=456@example.com";
await gotoRoutingLink({ page, queryString });
@ -167,8 +167,8 @@ test.describe("Routing Forms", () => {
// All other params come from prefill URL
expect(url.searchParams.get("Test Field Number")).toBe("456");
expect(url.searchParams.get("Test Field Long Text")).toBe("manual-fill");
expect(url.searchParams.get("Test Field Select")).toBe("456");
expect(url.searchParams.getAll("Test Field MultiSelect")).toMatchObject(["456", "789"]);
expect(url.searchParams.get("Test Field Multiple Selection")).toBe("456");
expect(url.searchParams.getAll("Test Field Multiple Selection")).toMatchObject(["456", "789"]);
expect(url.searchParams.get("Test Field Phone")).toBe("456");
expect(url.searchParams.get("Test Field Email")).toBe("456@example.com");
});
@ -447,7 +447,7 @@ async function addAllTypesOfFieldsAndSaveForm(
const { optionsInUi: fieldTypesList } = await verifySelectOptions(
{ selector: ".data-testid-field-type", nth: 0 },
["Email", "Long Text", "MultiSelect", "Number", "Phone", "Select", "Short Text"],
["Email", "Long Text", "Multiple Selection", "Number", "Phone", "Single Selection", "Short Text"],
page
);
@ -507,7 +507,7 @@ export async function addOneFieldAndDescriptionAndSaveForm(
// Verify all Options of SelectBox
const { optionsInUi: types } = await verifySelectOptions(
{ selector: ".data-testid-field-type", nth: 0 },
["Email", "Long Text", "MultiSelect", "Number", "Phone", "Select", "Short Text"],
["Email", "Long Text", "Multiple Selection", "Number", "Phone", "Single Selection", "Short Text"],
page
);

View File

@ -231,7 +231,11 @@ export const getBusyCalendarTimes = async (
const months = getMonths(dateFrom, dateTo);
try {
if (coldStart) {
results = await getCachedResults(withCredentials, dateFrom, dateTo, selectedCalendars);
// Subtract 11 hours from the start date to avoid problems in UTC- time zones.
const startDate = dayjs(dateFrom).subtract(11, "hours").format();
// Add 14 hours from the start date to avoid problems in UTC+ time zones.
const endDate = dayjs(dateTo).endOf("month").add(14, "hours").format();
results = await getCachedResults(withCredentials, startDate, endDate, selectedCalendars);
logger.info("Generating calendar cache in background");
// on cold start the calendar cache page generated in the background
Promise.all(months.map((month) => createCalendarCachePage(username, month)));

View File

@ -1,14 +1,13 @@
{
"extends": "@calcom/tsconfig/base.json",
"compilerOptions": {
"module": "ESNext",
"target": "ES2015",
"moduleResolution": "Node",
"baseUrl": ".",
"declaration": true,
"jsx": "preserve",
"outDir": "dist",
},
"include": ["**/*.ts"],
}
"extends": "@calcom/tsconfig/base.json",
"compilerOptions": {
"module": "ESNext",
"target": "ES2015",
"moduleResolution": "Node",
"baseUrl": ".",
"declaration": true,
"jsx": "preserve",
"outDir": "dist"
},
"include": ["**/*.ts"]
}

View File

@ -264,7 +264,7 @@ const ProfileView = () => {
<>
<Label className="text-emphasis mt-5">{t("about")}</Label>
<div
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600 break-words"
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: md.render(team.bio || "") }}
/>
</>

View File

@ -28,12 +28,6 @@ export const CheckedTeamSelect = ({
return (
<>
<Select
styles={{
option: (styles, { isDisabled }) => ({
...styles,
backgroundColor: isDisabled ? "#F5F5F5" : "inherit",
}),
}}
name={props.name}
placeholder={props.placeholder || t("select")}
isSearchable={true}

View File

@ -27,12 +27,6 @@ export const CheckedUserSelect = ({
return (
<>
<Select
styles={{
option: (styles, { isDisabled }) => ({
...styles,
backgroundColor: isDisabled ? "#F5F5F5" : "inherit",
}),
}}
name={props.name}
placeholder={props.placeholder || t("select")}
isSearchable={false}

View File

@ -40,12 +40,6 @@ export const ChildrenEventTypeSelect = ({
return (
<>
<Select
styles={{
option: (styles, { isDisabled }) => ({
...styles,
backgroundColor: isDisabled ? "#F5F5F5" : "inherit",
}),
}}
name={props.name}
placeholder={t("select")}
options={options}

View File

@ -12,4 +12,5 @@ export type AppFlags = {
"managed-event-types": boolean;
organizations: boolean;
"google-workspace-directory": boolean;
"disable-signup": boolean;
};

View File

@ -493,7 +493,7 @@ export const FormBuilder = function FormBuilder({
fieldForm.getValues("editable") === "system" ||
fieldForm.getValues("editable") === "system-but-optional"
}
label="Identifier"
label={t("identifier")}
/>
<InputField
{...fieldForm.register("label")}

View File

@ -10,6 +10,7 @@ import { classNames } from "@calcom/lib";
import { HOSTED_CAL_FEATURES, WEBAPP_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { IdentityProvider } from "@calcom/prisma/enums";
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import useAvatarQuery from "@calcom/trpc/react/hooks/useAvatarQuery";
@ -51,8 +52,8 @@ const tabs: VerticalTabItemProps[] = [
icon: Key,
children: [
{ name: "password", href: "/settings/security/password" },
{ name: "2fa_auth", href: "/settings/security/two-factor-auth" },
{ name: "impersonation", href: "/settings/security/impersonation" },
{ name: "2fa_auth", href: "/settings/security/two-factor-auth" },
],
},
{
@ -116,6 +117,10 @@ const useTabs = () => {
tab.name = user?.name || "my_account";
tab.icon = undefined;
tab.avatar = avatar?.avatar || WEBAPP_URL + "/" + session?.data?.user?.username + "/avatar.png";
} else if (tab.href === "/settings/security" && user?.identityProvider === IdentityProvider.GOOGLE) {
tab.children = tab?.children?.filter(
(childTab) => childTab.href !== "/settings/security/two-factor-auth"
);
}
return tab;
});

View File

@ -647,17 +647,18 @@ const NavigationItem: React.FC<{
href={item.href}
aria-label={t(item.name)}
className={classNames(
"hover:bg-emphasis [&[aria-current='page']]:bg-emphasis hover:text-emphasis text-default group flex items-center rounded-md py-2 px-3 text-sm font-medium",
"[&[aria-current='page']]:bg-emphasis text-default group flex items-center rounded-md py-2 px-3 text-sm font-medium",
isChild
? `[&[aria-current='page']]:text-emphasis hidden h-8 pl-16 lg:flex lg:pl-11 [&[aria-current='page']]:bg-transparent ${
props.index === 0 ? "mt-0" : "mt-px"
}`
: "[&[aria-current='page']]:text-emphasis mt-0.5 text-sm"
: "[&[aria-current='page']]:text-emphasis mt-0.5 text-sm",
isLocaleReady ? "hover:bg-emphasis hover:text-emphasis" : ""
)}
aria-current={current ? "page" : undefined}>
{item.icon && (
<item.icon
className="h-4 w-4 flex-shrink-0 ltr:mr-2 rtl:ml-2 [&[aria-current='page']]:text-inherit"
className="mr-2 h-4 w-4 flex-shrink-0 ltr:mr-2 rtl:ml-2 [&[aria-current='page']]:text-inherit"
aria-hidden="true"
aria-current={current ? "page" : undefined}
/>
@ -668,7 +669,7 @@ const NavigationItem: React.FC<{
{item.badge && item.badge}
</span>
) : (
<SkeletonText className="h-3 w-32" />
<SkeletonText style={{ width: `${item.name.length * 10}px` }} className="h-[20px]" />
)}
</Link>
</Tooltip>

View File

@ -89,6 +89,7 @@ export async function isTeamAdmin(userId: number, teamId: number) {
where: {
userId,
teamId,
accepted: true,
OR: [{ role: "ADMIN" }, { role: "OWNER" }],
},
})) || false
@ -99,6 +100,7 @@ export async function isTeamOwner(userId: number, teamId: number) {
where: {
userId,
teamId,
accepted: true,
role: "OWNER",
},
}));
@ -109,6 +111,7 @@ export async function isTeamMember(userId: number, teamId: number) {
where: {
userId,
teamId,
accepted: true,
},
}));
}

View File

@ -0,0 +1,10 @@
INSERT INTO
"Feature" (slug, enabled, description, "type")
VALUES
(
'disable-signup',
false,
'Enable to prevent users from signing up',
'OPERATIONAL'
) ON CONFLICT (slug) DO NOTHING;

View File

@ -4,6 +4,7 @@ import { ZAppByIdInputSchema } from "./appById.schema";
import { ZAppCredentialsByTypeInputSchema } from "./appCredentialsByType.schema";
import { ZAppsInputSchema } from "./apps.schema";
import { ZAwayInputSchema } from "./away.schema";
import { ZConnectedCalendarsInputSchema } from "./connectedCalendars.schema";
import { ZDeleteCredentialInputSchema } from "./deleteCredential.schema";
import { ZDeleteMeInputSchema } from "./deleteMe.schema";
import { ZEventTypeOrderInputSchema } from "./eventTypeOrder.schema";
@ -110,7 +111,7 @@ export const loggedInViewerRouter = router({
return UNSTABLE_HANDLER_CACHE.away({ ctx, input });
}),
connectedCalendars: authedProcedure.query(async ({ ctx }) => {
connectedCalendars: authedProcedure.input(ZConnectedCalendarsInputSchema).query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.connectedCalendars) {
UNSTABLE_HANDLER_CACHE.connectedCalendars = (
await import("./connectedCalendars.handler")
@ -122,7 +123,7 @@ export const loggedInViewerRouter = router({
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.connectedCalendars({ ctx });
return UNSTABLE_HANDLER_CACHE.connectedCalendars({ ctx, input });
}),
setDestinationCalendar: authedProcedure

View File

@ -5,14 +5,18 @@ import { prisma } from "@calcom/prisma";
import { AppCategories } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TConnectedCalendarsInputSchema } from "./connectedCalendars.schema";
type ConnectedCalendarsOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TConnectedCalendarsInputSchema;
};
export const connectedCalendarsHandler = async ({ ctx }: ConnectedCalendarsOptions) => {
export const connectedCalendarsHandler = async ({ ctx, input }: ConnectedCalendarsOptions) => {
const { user } = ctx;
const onboarding = input?.onboarding || false;
const userCredentials = await prisma.credential.findMany({
where: {
@ -33,6 +37,12 @@ export const connectedCalendarsHandler = async ({ ctx }: ConnectedCalendarsOptio
user.selectedCalendars,
user.destinationCalendar?.externalId
);
let toggledCalendarDetails:
| {
externalId: string;
integration: string;
}
| undefined;
if (connectedCalendars.length === 0) {
/* As there are no connected calendars, delete the destination calendar if it exists */
@ -48,6 +58,19 @@ export const connectedCalendarsHandler = async ({ ctx }: ConnectedCalendarsOptio
So create a default destination calendar with the first primary connected calendar
*/
const { integration = "", externalId = "", credentialId } = connectedCalendars[0].primary ?? {};
// Select the first calendar matching the primary by default since that will also be the destination calendar
if (onboarding && externalId) {
const calendarIndex = (connectedCalendars[0].calendars || []).findIndex(
(item) => item.externalId === externalId && item.integration === integration
);
if (calendarIndex >= 0 && connectedCalendars[0].calendars) {
connectedCalendars[0].calendars[calendarIndex].isSelected = true;
toggledCalendarDetails = {
externalId,
integration,
};
}
}
user.destinationCalendar = await prisma.destinationCalendar.create({
data: {
userId: user.id,
@ -70,6 +93,19 @@ export const connectedCalendarsHandler = async ({ ctx }: ConnectedCalendarsOptio
if (!destinationCal) {
// If destinationCalendar is out of date, update it with the first primary connected calendar
const { integration = "", externalId = "" } = connectedCalendars[0].primary ?? {};
// Select the first calendar matching the primary by default since that will also be the destination calendar
if (onboarding && externalId) {
const calendarIndex = (connectedCalendars[0].calendars || []).findIndex(
(item) => item.externalId === externalId && item.integration === integration
);
if (calendarIndex >= 0 && connectedCalendars[0].calendars) {
connectedCalendars[0].calendars[calendarIndex].isSelected = true;
toggledCalendarDetails = {
externalId,
integration,
};
}
}
user.destinationCalendar = await prisma.destinationCalendar.update({
where: { userId: user.id },
data: {
@ -77,9 +113,49 @@ export const connectedCalendarsHandler = async ({ ctx }: ConnectedCalendarsOptio
externalId,
},
});
} else if (onboarding && !destinationCal.isSelected) {
// Mark the destination calendar as selected in the calendar list
// We use every so that we can exit early once we find the matching calendar
connectedCalendars.every((cal) => {
const index = (cal.calendars || []).findIndex(
(calendar) =>
calendar.externalId === destinationCal.externalId &&
calendar.integration === destinationCal.integration
);
if (index >= 0 && cal.calendars) {
cal.calendars[index].isSelected = true;
toggledCalendarDetails = {
externalId: destinationCal.externalId,
integration: destinationCal.integration || "",
};
return false;
}
return true;
});
}
}
// Insert the newly toggled record to the DB
if (toggledCalendarDetails) {
await prisma.selectedCalendar.upsert({
where: {
userId_integration_externalId: {
userId: user.id,
integration: toggledCalendarDetails.integration,
externalId: toggledCalendarDetails.externalId,
},
},
create: {
userId: user.id,
integration: toggledCalendarDetails.integration,
externalId: toggledCalendarDetails.externalId,
},
// already exists
update: {},
});
}
return {
connectedCalendars,
destinationCalendar: {

View File

@ -1 +1,9 @@
export {};
import { z } from "zod";
export const ZConnectedCalendarsInputSchema = z
.object({
onboarding: z.boolean().optional(),
})
.optional();
export type TConnectedCalendarsInputSchema = z.infer<typeof ZConnectedCalendarsInputSchema>;

View File

@ -1,6 +1,7 @@
import classNames from "classnames";
import type { ReactNode } from "react";
import { forwardRef } from "react";
import type { IconType } from "react-icons";
import { CheckCircle2, Info, XCircle, AlertTriangle } from "@calcom/ui/components/icon";
@ -14,9 +15,10 @@ export interface AlertProps {
iconClassName?: string;
// @TODO: Success and info shouldn't exist as per design?
severity: "success" | "warning" | "error" | "info" | "neutral";
CustomIcon?: IconType;
}
export const Alert = forwardRef<HTMLDivElement, AlertProps>((props, ref) => {
const { severity, iconClassName } = props;
const { severity, iconClassName, CustomIcon } = props;
return (
<div
@ -31,38 +33,45 @@ export const Alert = forwardRef<HTMLDivElement, AlertProps>((props, ref) => {
severity === "neutral" && "bg-subtle text-default border-none"
)}>
<div className="relative flex flex-col md:flex-row">
<div className="flex-shrink-0">
{severity === "error" && (
<XCircle
className={classNames("h-5 w-5 fill-red-400 text-white", iconClassName)}
aria-hidden="true"
/>
)}
{severity === "warning" && (
<AlertTriangle
className={classNames("h-5 w-5 fill-yellow-400 text-white", iconClassName)}
aria-hidden="true"
/>
)}
{severity === "info" && (
<Info
className={classNames("h-5 w-5 fill-sky-400 text-white", iconClassName)}
aria-hidden="true"
/>
)}
{severity === "neutral" && (
<Info
className={classNames("text-default h-5 w-5 fill-transparent", iconClassName)}
aria-hidden="true"
/>
)}
{severity === "success" && (
<CheckCircle2
className={classNames("fill-muted h-5 w-5 text-white", iconClassName)}
aria-hidden="true"
/>
)}
</div>
{CustomIcon ? (
<div className="flex-shrink-0">
<CustomIcon aria-hidden="true" className={classNames("text-default h-5 w-5", iconClassName)} />
</div>
) : (
<div className="flex-shrink-0">
{severity === "error" && (
<XCircle
className={classNames("h-5 w-5 fill-red-400 text-white", iconClassName)}
aria-hidden="true"
/>
)}
{severity === "warning" && (
<AlertTriangle
className={classNames("h-5 w-5 fill-yellow-400 text-white", iconClassName)}
aria-hidden="true"
/>
)}
{severity === "info" && (
<Info
className={classNames("h-5 w-5 fill-sky-400 text-white", iconClassName)}
aria-hidden="true"
/>
)}
{severity === "neutral" && (
<Info
className={classNames("text-default h-5 w-5 fill-transparent", iconClassName)}
aria-hidden="true"
/>
)}
{severity === "success" && (
<CheckCircle2
className={classNames("fill-muted h-5 w-5 text-white", iconClassName)}
aria-hidden="true"
/>
)}
</div>
)}
<div className="ml-3 flex-grow">
<h3 className="text-sm font-medium">{props.title}</h3>
<div className="text-sm">{props.message}</div>

View File

@ -5,7 +5,7 @@ import { classNames } from "@calcom/lib";
import type { BadgeProps } from "../..";
import { Badge } from "../..";
import { Divider } from "../divider";
import { ArrowDown, ArrowUp, Trash } from "../icon";
import { ArrowDown, ArrowUp, Trash2 } from "../icon";
type Action = { check: () => boolean; fn: () => void };
export default function FormCard({
@ -68,7 +68,7 @@ export default function FormCard({
deleteField?.fn();
}}
color="secondary">
<Trash className="text-muted h-4 w-4" />
<Trash2 className="text-default h-4 w-4" />
</button>
) : null}
</div>

View File

@ -44,6 +44,7 @@ export const Select = <
cx(
"bg-default flex cursor-pointer justify-between py-2.5 px-3 rounded-none text-default ",
state.isFocused && "bg-subtle",
state.isDisabled && "bg-muted",
state.isSelected && "bg-emphasis text-default",
props.classNames?.option
),

View File

@ -10,18 +10,38 @@ import { Label } from "../../../components/form/inputs/Label";
const boolean = (yesNo: "yes" | "no") => (yesNo === "yes" ? true : yesNo === "no" ? false : undefined);
const yesNo = (boolean?: boolean) => (boolean === true ? "yes" : boolean === false ? "no" : undefined);
type VariantStyles = {
commonClass?: string;
toggleGroupPrimitiveClass?: string;
};
const getVariantStyles = (variant: string) => {
const variants: Record<string, VariantStyles> = {
default: {
commonClass: "px-4 w-full py-[10px]",
},
small: {
commonClass: "w-[49px] px-3 py-1.5",
toggleGroupPrimitiveClass: "space-x-1",
},
};
return variants[variant];
};
export const BooleanToggleGroup = function BooleanToggleGroup({
defaultValue = true,
value,
disabled = false,
// eslint-disable-next-line @typescript-eslint/no-empty-function
onValueChange = () => {},
variant = "default",
...passThrough
}: {
defaultValue?: boolean;
value?: boolean;
onValueChange?: (value?: boolean) => void;
disabled?: boolean;
variant?: "default" | "small";
}) {
// Maintain a state because it is not necessary that onValueChange the parent component would re-render. Think react-hook-form
// Also maintain a string as boolean isn't accepted as ToggleGroupPrimitive value
@ -33,9 +53,11 @@ export const BooleanToggleGroup = function BooleanToggleGroup({
return null;
}
const commonClass = classNames(
"w-full inline-flex items-center justify-center rounded py-[10px] px-4 text-sm font-medium leading-4",
getVariantStyles(variant).commonClass,
"inline-flex items-center justify-center rounded text-sm font-medium leading-4",
disabled && "cursor-not-allowed"
);
const selectedClass = classNames(commonClass, "bg-emphasis text-emphasis");
const unselectedClass = classNames(commonClass, "text-default hover:bg-subtle hover:text-emphasis");
return (
@ -43,7 +65,10 @@ export const BooleanToggleGroup = function BooleanToggleGroup({
value={yesNoValue}
type="single"
disabled={disabled}
className="border-subtle flex h-9 space-x-2 rounded-md border p-1 rtl:space-x-reverse"
className={classNames(
"border-subtle flex h-9 space-x-2 rounded-md border p-1 rtl:space-x-reverse",
getVariantStyles(variant).toggleGroupPrimitiveClass
)}
onValueChange={(yesNoValue: "yes" | "no") => {
setYesNoValue(yesNoValue);
onValueChange(boolean(yesNoValue));

View File

@ -31,7 +31,7 @@ const HorizontalTabItem = function ({ name, href, linkProps, avatar, ...props }:
href={href}
{...linkProps}
className={classNames(
isCurrent ? "bg-subtle text-emphasis" : " hover:bg-subtle hover:text-emphasis text-default ",
isCurrent ? "bg-emphasis text-emphasis" : "hover:bg-subtle hover:text-emphasis text-default",
"inline-flex items-center justify-center whitespace-nowrap rounded-[6px] p-2 text-sm font-medium leading-4 md:mb-0",
props.disabled && "pointer-events-none !opacity-30",
props.className

View File

@ -15,7 +15,7 @@ const HorizontalTabs = function ({ tabs, linkProps, actions, ...props }: NavTabP
aria-label="Tabs"
{...props}>
{tabs.map((tab, idx) => (
<HorizontalTabItem {...tab} key={idx} {...linkProps} />
<HorizontalTabItem className="py-2.5 px-4" {...tab} key={idx} {...linkProps} />
))}
</nav>
{actions && actions}

View File

@ -62,12 +62,14 @@ const Skeleton = <T extends keyof JSX.IntrinsicElements | React.FC>({
);
};
const SkeletonText: React.FC<SkeletonBaseProps & { invisible?: boolean }> = ({
const SkeletonText: React.FC<SkeletonBaseProps & { invisible?: boolean; style?: React.CSSProperties }> = ({
className = "",
invisible = false,
style,
}) => {
return (
<span
style={style}
className={classNames(
`font-size-0 bg-emphasis inline-block animate-pulse rounded-md empty:before:inline-block empty:before:content-['']`,
className,