Merge branch 'main' into integromat-app
commit
f1120334f8
|
@ -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
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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é",
|
||||
|
|
|
@ -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": "צור/צרי עבור"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "此预约的退款正在进行中。",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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 || "") }}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -11,4 +11,5 @@ export type AppFlags = {
|
|||
"v2-booking-page": boolean;
|
||||
"managed-event-types": boolean;
|
||||
"google-workspace-directory": boolean;
|
||||
"disable-signup": boolean;
|
||||
};
|
||||
|
|
|
@ -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")}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue