Merge branch 'main' into integromat-app

integromat-app
aar2dee2 2023-06-06 10:43:04 +05:30
commit f1120334f8
37 changed files with 554 additions and 147 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

@ -104,9 +104,9 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
</div>
</div>
{!shouldLockDisableProps("apps").disabled && (
<div>
<div className="bg-muted rounded-md p-8">
{!isLoading && notInstalledApps?.length ? (
<h2 className="text-emphasis my-2 text-lg font-semibold">{t("available_apps")}</h2>
<h2 className="text-emphasis text-lg font-semibold">{t("available_apps")}</h2>
) : null}
<div className="before:border-0">
{notInstalledApps?.map((app) => (

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

@ -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

@ -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

@ -1676,6 +1676,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.",
@ -1718,6 +1719,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

@ -604,6 +604,11 @@
"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": "לקריאה בלבד",
@ -753,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}} שלך? כל מי ששיתפת איתו את הקישור לחשבון לא יוכל יותר להזמין באמצעותו, וכל ההעדפות ששמרת יאבדו.",
@ -900,6 +914,7 @@
"duplicate": "כפילות",
"offer_seats": "הצעת מקומות",
"offer_seats_description": "הצעת מקומות להזמנות (אפשרות זו משביתה באופן אוטומטי אישורי הזמנות ואורחים).",
"seats_available_one": "מקום זמין",
"seats_available_other": "מקומות זמינים",
"number_of_seats": "מספר המקומות לכל הזמנה",
"enter_number_of_seats": "יש להזין את מספר המקומות",
@ -1034,6 +1049,7 @@
"event_cancelled_trigger": "כאשר אירוע מבוטל",
"new_event_trigger": "כאשר אירוע חדש מוזמן",
"email_host_action": "שלח מייל למארח",
"email_attendee_action": "שליחת דוא\"ל למשתתפים",
"sms_attendee_action": "שלח SMS למשתתף",
"sms_number_action": "שלח SMS למספר מסוים",
"workflows": "תהליכים",
@ -1144,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": "חריגה מהגבלת קצב",
@ -1184,6 +1202,7 @@
"create_workflow": "יצירת תהליך עבודה",
"do_this": "עשה את זה",
"turn_off": "כבה",
"turn_on": "הפעלה",
"settings_updated_successfully": "עדכון ההגדרות בוצע בהצלחה",
"error_updating_settings": "שגיאה בעדכון ההגדרות",
"personal_cal_url": "כתובת ה-URL האישית שלי של {{appName}}",
@ -1194,6 +1213,7 @@
"start_of_week": "תחילת השבוע",
"recordings_title": "הקלטות",
"recording": "הקלטה",
"happy_scheduling": "תזמון נעים!",
"select_calendars": "בחר את לוחות השנה שבהם ברצונך לבדוק אם יש התנגשויות, כדי למנוע כפל הזמנות.",
"check_for_conflicts": "בדיקת התנגשויות",
"view_recordings": "צפייה בהקלטות",
@ -1231,6 +1251,7 @@
"impersonation": "התחזות",
"impersonation_description": "הגדרות לניהול התחזות למשתמשים",
"users": "משתמשים",
"user": "משתמש",
"profile_description": "ניהול הגדרות עבור פרופיל {{appName}} שלך",
"users_description": "כאן אפשר למצוא רשימה של כל המשתמשים",
"users_listing": "רשימת משתמשים",
@ -1238,6 +1259,7 @@
"calendars_description": "הגדרת האינטראקציה בין סוגי האירועים שלך לבין לוחות השנה שלך",
"appearance_description": "ניהול ההגדרות של מראה ההזמנות שלך",
"conferencing_description": "הוסף/הוסיפי את האפליקציות האהובות עליך/ייך לשיחות ועידה לשימוש בפגישות",
"add_conferencing_app": "הוספת אפליקציה לשיחות ועידה",
"password_description": "נהל/י את ההגדרות של סיסמאות החשבון שלך",
"2fa_description": "נהל/י את ההגדרות של סיסמאות החשבון שלך",
"we_just_need_basic_info": "אנחנו זקוקים רק לפרטים בסיסיים כדי להגדיר את הפרופיל שלך.",
@ -1271,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 הראשון שלך",
@ -1307,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": "דוא\"ל נשלח לבעלים לאחר שליחת הטופס",
@ -1361,6 +1394,7 @@
"add_limit": "הוספת הגבלה",
"team_name_required": "נדרש שם צוות",
"show_attendees": "שיתוף האורחים בפרטי המשתתפים",
"how_booking_questions_as_variables": "איך משתמשים בשאלות הזמנה בתור משתנים?",
"format": "פורמט",
"uppercase_for_letters": "שימוש באותיות רישיות עבור כל האותיות",
"replace_whitespaces_underscores": "החלפת רווחים במקפים תחתונים",
@ -1375,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": "החיבור ללוח השנה נכשל",
@ -1457,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}}.",
@ -1510,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": "הקוד לאימות נשלח",
@ -1523,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": "הקצאה",
@ -1555,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/לחודש",
@ -1590,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": "קשר את לוח השנה שלך",
@ -1616,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}} מושבים",
@ -1630,6 +1676,7 @@
"not_enough_seats": "אין מספיק מושבים",
"form_builder_field_already_exists": "שדה עם שם זה כבר קיים",
"form_builder_field_add_subtitle": "התאמת השאלות שנשאלות בדף התזמון",
"show_on_booking_page": "להציג בדף ההזמנות",
"get_started_zapier_templates": "התחל עם תבניות Zapier",
"team_is_unpublished": "צוות {{team}} אינו מפורסם",
"team_is_unpublished_description": "קישור הצוות הזה אינו זמין כעת. אנא צור קשר עם הבעלים של הצוות או בקש מהם לפרסם אותו.",
@ -1663,6 +1710,15 @@
"spot_popular_event_types_description": "צפה איזה ארועים מקבלים הכי הרבה גישות ותזמונים",
"no_responses_yet": "אין עדיין תגובות",
"this_will_be_the_placeholder": "זה יהיה הממלא מקום",
"error_booking_event": "אירעה שגיאה בעת הזמנת האירוע, רענן/י את הדף ונסה/י שוב",
"timeslot_missing_title": "לא נבחר חלון זמן",
"timeslot_missing_description": "כדי להזמין את האירוע, יש לבחור חלון זמן.",
"timeslot_missing_cta": "חלון הזמן שנבחר",
"switch_monthly": "מעבר לתצוגת חודש",
"switch_weekly": "מעבר לתצוגת שבוע",
"switch_multiday": "מעבר לתצוגת יום",
"num_locations": "{{num}} אפשרויות מיקום",
"select_on_next_step": "בחר/י בשלב הבא",
"this_meeting_has_not_started_yet": "פגישה זו עוד לא התחילה",
"this_app_requires_connected_account": "{{appName}} דורש חשבון {{dependencyName}} מחובר",
"connect_app": "חבר את {{dependencyName}}",
@ -1672,12 +1728,39 @@
"can_you_try_again": "אתה יכול לנסות שוב עם שעה אחרת?",
"verify": "אמת",
"timezone_variable": "אזור זמן",
"timezone_info": "אזור הזמן של האדם שיקבל את ההזמנה",
"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": "מיקום ברירת המחדל של החבר/ה",
"members_default_schedule_description": "אנחנו נשתמש בלוח הזמנים לזמינות שמוגדר כברירת מחדל עבור כל חבר/ה. החברים יוכלו לערוך את הנתונים.",
"requires_at_least_one_schedule": "אתה חייב שיהיה לך לפחות לוח זמנים אחד",
"default_conferencing_bulk_description": "עדכן את המיקומים עבור סוגי הארועים שנבחרו",
"locked_for_members": "נעול לחברים",
"locked_apps_description": "החברים יוכלו לראות את האפליקציות הפעילות, אבל לא יוכלו לערוך הגדרות של האפליקציה",
"locked_webhooks_description": "החברים יוכלו לראות את רכיבי ה-Webhook הפעילים, אבל לא יוכלו לערוך הגדרות של רכיבי Webhook",
"locked_workflows_description": "החברים יוכלו לראות את תהליכי העבודה הפעילים, אבל לא יוכלו לערוך הגדרות של תהליכי עבודה",
"locked_by_admin": "נעול על ידי מנהל/ת הצוות",
"app_not_connected": "לא קישרת חשבון {{appName}}.",
"connect_now": "להתחבר עכשיו",
"managed_event_dialog_confirm_button_one": "החלפה ועדכון של חבר/ה {{count}}",
"managed_event_dialog_confirm_button_other": "החלפה ועדכון של {{count}} חברים",
"managed_event_dialog_title_one": "כתובת ה-URL /{{slug}} כבר קיימת עבור חבר/ה {{count}}. האם ברצונך להחליף אותה?",
"managed_event_dialog_title_other": "כתובת ה-URL /{{slug}} כבר קיימת עבור {{count}} חברים. האם ברצונך להחליף אותה?",
"managed_event_dialog_information_one": "<strong>{{names}}</strong> כבר משתמש/ת בכתובת ה-URL <strong>/{{slug}}</strong>.",
"managed_event_dialog_information_other": "<strong>{{names}}</strong> כבר משתמשים בכתובת ה-URL <string>/{{slug}}</strong>.",
"managed_event_dialog_clarification": "אם תבחר/י להחליף את כתובת ה-URL, נודיע לחברים. אם אינך רוצה להחליף אותה, חזור/י אחורה והסר/י את החברים.",
"review_event_type": "בדיקת סוג האירוע",
"looking_for_more_analytics": "מחפש עוד מידע אנליטי?",
"looking_for_more_insights": "רצית עוד Insights?",
"add_filter": "הוסף סנן",
"select_user": "בחר משתמש",
"select_event_type": "בחר סוג ארוע",
@ -1693,5 +1776,64 @@
"events_rescheduled": "ארועים שתוזמנו מחדש",
"from_last_period": "מפרק הזמן האחרון",
"from_to_date_period": "מ: {{startDate}} עד: {{endDate}}",
"event_trends": "מגמות ארוע"
"analytics_for_organisation": "Insights",
"subtitle_analytics": "קבל/י מידע נוסף על הפעילות של הצוות שלך",
"redirect_url_warning": "הוספת כתובת URL להפניה אוטומטית תגרום להשבתת דף 'הפעולה הצליחה'. חשוב להוסיף את ההודעה 'ההזמנה אושרה בהצלחה' בדף המותאם אישית לציון שהפעולה הצליחה.",
"event_trends": "מגמות ארוע",
"clear_filters": "ניקוי המסננים",
"hold": "החזקה",
"on_booking_option": "גביית תשלום בעת ההזמנה",
"hold_option": "חיוב דמי אי-הגעה",
"card_held": "החזקת הסכום בכרטיס",
"charge_card": "חיוב הכרטיס",
"card_charged": "הכרטיס חויב",
"no_show_fee_amount": "דמי אי-הגעה בסך {{amount, currency}}",
"no_show_fee": "דמי אי-הגעה",
"submit_card": "מסירת כרטיס",
"submit_payment_information": "מסירת פרטי כרטיס",
"meeting_awaiting_payment_method": "עוד לא צוין אמצעי תשלום עבור הפגישה",
"no_show_fee_charged_email_subject": "דמי אי-הגעה בסך {{amount, currency}} חויבו עבור {{title}} ב-{{date}}",
"no_show_fee_charged_text_body": "דמי אי-הגעה חויבו",
"no_show_fee_charged_subtitle": "דמי אי-הגעה בסך {{amount, currency}} חויבו עבור האירוע הבא",
"error_charging_card": "משהו השתבש בעת גביית התשלום על אי-הגעה. אפשר לנסות שוב מאוחר יותר.",
"collect_no_show_fee": "גביית דמי אי-הגעה",
"no_show_fee_charged": "דמי אי-הגעה חויבו",
"insights": "Insights",
"testing_workflow_info_message": "במהלך בדיקת תהליך העבודה הזה, קח/י בחשבון שהודעות דוא\"ל ו-SMS ניתנות לתזמון לפחות שעה מראש",
"insights_no_data_found_for_filter": "לא נמצאו נתונים עבור המסנן שנבחר או התאריכים שנבחרו.",
"acknowledge_booking_no_show_fee": "מובן לי שאם לא אשתתף באירוע הזה, דמי אי-הגעה בסך {{amount, currency}} ינוכו מהכרטיס שלי.",
"card_details": "פרטי כרטיס",
"seats_and_no_show_fee_error": "נכון לעכשיו, אי אפשר להפעיל מקומות ולחייב דמי אי-הגעה",
"complete_your_booking": "יש להשלים את ההזמנה",
"complete_your_booking_subject": "יש להשלים את ההזמנה: {{title}} ב-{{date}}",
"confirm_your_details": "אישור הפרטים שלך",
"currency_string": "{{amount, currency}}",
"charge_card_dialog_body": "את/ה עומד/ת לחייב את המשתתף/ת בסכום של {{amount, currency}}. בטוח שברצונך להמשיך?",
"charge_attendee": "לחייב את המשתתף/ת ב-{{amount, currency}}",
"payment_app_commission": "דרישת תשלום ({{paymentFeePercentage}}% + {{fee, currency}} עמלה על העסקה)",
"email_invite_team": "נשלחה הזמנה ל-{{email}}",
"email_invite_team_bulk": "{{userCount}} משתמשים הוזמנו",
"error_collecting_card": "שגיאה בחיוב הכרטיס",
"image_size_limit_exceed": "התמונה שתעלה/י יכולה להיות בנפח של מקסימום 5MB",
"inline_embed": "הטמעה מוטבעת",
"load_inline_content": "סוג האירוע נטען ישירות בתוך שאר תוכן האתר שלך.",
"floating_pop_up_button": "לחצן קופץ צף",
"floating_button_trigger_modal": "אפשרות זו מציבה באתר שלך לחצן צף שמפעיל חלון מודאלי עם סוג האירוע שלך.",
"pop_up_element_click": "קופץ בעקבות לחיצה על הרכיב",
"open_dialog_with_element_click": "פתיחת תיבת הדו-שיח ב-Cal כשמישהו לוחץ על רכיב.",
"need_help_embedding": "זקוק/ה לעזרה? ניתן לעיין במדריכים שלנו בנושא הטמעת Cal ב-Wix, ב-Squarespace או ב-WordPress, לקרוא את דף השאלות הנפוצות או לסקור אפשרויות הטמעה מתקדמות.",
"book_my_cal": "הזמנה ביומן שלי",
"invite_as": "הזמנה בְתור",
"form_updated_successfully": "עדכון הטופס בוצע בהצלחה.",
"email_not_cal_member_cta": "הצטרף/י לצוות שלך",
"disable_attendees_confirmation_emails": "השבתת הודעות דוא\"ל לאישור שנשלחות כברירת מחדל עבור המשתתפים",
"disable_attendees_confirmation_emails_description": "בסוג האירוע הזה פעיל לפחות תהליך עבודה אחד ששולח דוא\"ל למשתתפים כשהאירוע מוזמן.",
"disable_host_confirmation_emails": "השבתת הודעות דוא\"ל לאישור שנשלחות כברירת מחדל עבור המארח/ת",
"disable_host_confirmation_emails_description": "בסוג האירוע הזה פעיל לפחות תהליך עבודה אחד ששולח דוא\"ל למארח/ת כשהאירוע מוזמן.",
"add_an_override": "הוספת מעקף",
"import_from_google_workspace": "ייבוא משתמשים מ-Google Workspace",
"connect_google_workspace": "חיבור Google Workspace",
"google_workspace_admin_tooltip": "עליך להיות אדמין/ית ב-Workspace כדי להשתמש בתכונה הזו",
"first_event_type_webhook_description": "צור/צרי את ה-Webhook הראשון שלך עבור סוג זה של אירוע",
"create_for": "צור/צרי עבור"
}

View File

@ -1634,7 +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 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",

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": "此预约的退款正在进行中。",

View File

@ -29,20 +29,30 @@ export default function AppCard({
const [animationRef] = useAutoAnimate<HTMLDivElement>();
return (
<div className={`border-subtle mb-4 mt-2 rounded-md border ${!app.enabled && "grayscale"}`}>
<div className="p-4 text-sm sm:p-6">
<div
className={classNames(
"border-subtle mb-4",
app.isInstalled ? "mt-2" : "mt-6",
"rounded-md border",
!app.enabled && "grayscale",
"bg-red-400"
)}>
<div className={classNames(app.isInstalled ? "p-4 text-sm sm:p-4" : "px-5 py-4 text-sm sm:px-5")}>
<div className="flex w-full flex-col gap-2 sm:flex-row sm:gap-0">
{/* Don't know why but w-[42px] isn't working, started happening when I started using next/dynamic */}
<Link href={"/apps/" + app.slug} className="mr-3 h-auto w-10 rounded-sm">
<img
className={classNames(app?.logo.includes("-dark") && "dark:invert", "w-full min-w-[40px]")}
className={classNames(
app?.logo.includes("-dark") && "dark:invert",
`w-full ${app.isInstalled ? "min-w-[42px]" : "min-w-[32.47px]"}`
)}
src={app?.logo}
alt={app?.name}
/>
</Link>
<div className="flex flex-col">
<span className="text-emphasis text-base font-semibold leading-4">{app?.name}</span>
<p className="text-default mb-2 pt-2 text-sm font-normal ltr:pr-2 rtl:pl-2">
<p className="text-default max-w-md truncate pt-2 text-sm font-normal ltr:pr-2 rtl:pl-2">
{description || app?.description}
</p>
</div>
@ -70,7 +80,7 @@ export default function AppCard({
</div>
<div ref={animationRef}>
{app?.isInstalled && switchChecked && <hr className="border-subtle" />}
{app?.isInstalled && switchChecked ? <div className="p-4 text-sm sm:px-8">{children}</div> : null}
{app?.isInstalled && switchChecked ? <div className="p-4 text-sm sm:px-4">{children}</div> : null}
</div>
</div>
);

View File

@ -20,7 +20,7 @@ const RainbowInstallForm: React.FC<RainbowInstallFormProps> = ({
return (
<>
<div className="mt-4 block items-center sm:flex">
<div className="mt-5 block items-center sm:flex">
<div className="min-w-48 mb-4 sm:mb-0">
<label htmlFor="blockchainId" className="text-default flex text-sm font-medium">
{t("Blockchain")}
@ -36,7 +36,7 @@ const RainbowInstallForm: React.FC<RainbowInstallFormProps> = ({
options={SUPPORTED_CHAINS_FOR_FORM || [{ value: 1, label: "Ethereum" }]}
/>
</div>
<div className="block items-center sm:flex">
<div className="mt-5 block items-center pb-4 sm:flex">
<div className="min-w-48 mb-4 sm:mb-0">
<label htmlFor="smartContractAddress" className="text-default flex text-sm font-medium">
{t("token_address")}
@ -46,7 +46,7 @@ const RainbowInstallForm: React.FC<RainbowInstallFormProps> = ({
<div className="relative mt-1 rounded-sm">
<input
type="text"
className="border-default block w-full rounded-sm text-sm "
className="border-default block w-full rounded-sm text-sm"
placeholder={t("Example: 0x71c7656ec7ab88b098defb751b7401b5f6d8976f")}
defaultValue={(smartContractAddress || "") as string}
onChange={(e) => {

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

@ -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

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

View File

@ -377,7 +377,7 @@ export const FormBuilder = function FormBuilder({
onCheckedChange={(checked) => {
update(index, { ...field, hidden: !checked });
}}
classNames={{ container: "p-2 hover:bg-gray-100 rounded" }}
classNames={{ container: "p-2 hover:bg-subtle rounded" }}
tooltip={t("show_on_booking_page")}
/>
)}
@ -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

@ -646,17 +646,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}
/>
@ -667,7 +668,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

@ -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

@ -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,