Merge remote-tracking branch 'origin/main' into feature/booking-filters

# Conflicts:
#	apps/api
#	apps/web/public/static/locales/en/common.json
#	apps/website
#	packages/ui/components/avatar/Avatar.tsx
#	packages/ui/index.tsx
#	packages/ui/v2/core/form/index.ts
#	packages/ui/v2/core/form/select/index.ts
#	packages/ui/v2/core/index.ts
This commit is contained in:
sean-brydon 2022-12-19 11:05:53 +00:00
commit 17afc454fa
96 changed files with 1798 additions and 543 deletions

@ -1 +1 @@
Subproject commit c129586336b287d7b93c516435d905337951d2d2
Subproject commit 12f19ff7c03a5284d2740e01d5291b713f120a21

View File

@ -189,3 +189,7 @@
padding: 24px !important;
}
}
.docs-story {
padding: 24px !important;
}

View File

@ -44,7 +44,7 @@ function IntegrationListItem(props: {
expanded={!!props.children}
className={classNames(
props.separate ? "rounded-md" : "first:rounded-t-md last:rounded-b-md",
"my-0 flex-col border transition-colors duration-500 ",
"my-0 flex-col border transition-colors duration-500",
highlight ? "bg-yellow-100" : ""
)}>
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>

View File

@ -57,7 +57,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
</span>
<span className="text-bookinglight font-medium">
, {date.toDate().toLocaleString(i18n.language, { month: "long" })} {date.format(" D ")}
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
</span>
</div>
<div className="ml-auto">

View File

@ -348,7 +348,7 @@ function BookingListItem(booking: BookingItemProps) {
</div>
{booking.description && (
<div
className="max-w-10/12 sm:max-w-40 md:max-w-56 xl:max-w-80 lg:max-w-64 truncate text-sm text-gray-600"
className="max-w-10/12 sm:max-w-32 md:max-w-52 xl:max-w-80 truncate text-sm text-gray-600"
title={booking.description}>
&quot;{booking.description}&quot;
</div>

View File

@ -299,6 +299,39 @@ const BookingPage = ({
}
const bookEvent = (booking: BookingFormValues) => {
const bookingCustomInputs = Object.keys(booking.customInputs || {}).map((inputId) => ({
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
value: booking.customInputs && booking.customInputs[inputId] ? booking.customInputs[inputId] : "",
}));
// Checking if custom inputs of type Phone number are valid to display error message on UI
if (eventType.customInputs.length) {
let isErrorFound = false;
eventType.customInputs.forEach((customInput) => {
if (customInput.required && customInput.type === EventTypeCustomInputType.PHONE) {
const input = bookingCustomInputs.find((i) => i.label === customInput.label);
try {
z.string({
errorMap: () => ({
message: `Missing ${customInput.type} customInput: '${customInput.label}'`,
}),
})
.refine((val) => isValidPhoneNumber(val), {
message: "Phone number is invalid",
})
.parse(input?.value);
} catch (err) {
isErrorFound = true;
bookingForm.setError(`customInputs.${customInput.id}`, {
type: "custom",
message: "Invalid Phone number",
});
}
}
});
if (isErrorFound) return;
}
telemetry.event(
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
{ isTeamBooking: document.URL.includes("team/") }
@ -355,10 +388,7 @@ const BookingPage = ({
attendeeAddress: booking.attendeeAddress,
}),
metadata,
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
value: booking.customInputs && inputId in booking.customInputs ? booking.customInputs[inputId] : "",
})),
customInputs: bookingCustomInputs,
hasHashedBookingLink,
hashedLink,
smsReminderNumber:
@ -386,10 +416,7 @@ const BookingPage = ({
attendeeAddress: booking.attendeeAddress,
}),
metadata,
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
value: booking.customInputs && inputId in booking.customInputs ? booking.customInputs[inputId] : "",
})),
customInputs: bookingCustomInputs,
hasHashedBookingLink,
hashedLink,
smsReminderNumber:
@ -793,6 +820,23 @@ const BookingPage = ({
</Group>
</div>
)}
{input.type === EventTypeCustomInputType.PHONE && (
<div>
<PhoneInput<BookingFormValues>
name={`customInputs.${input.id}`}
control={bookingForm.control}
placeholder={t("enter_phone_number")}
id={`customInputs.${input.id}`}
required={input.required}
/>
{bookingForm.formState.errors?.customInputs?.[input.id] && (
<div className="mt-2 flex items-center text-sm text-red-700 ">
<Icon.FiInfo className="mr-2 h-3 w-3" />
<p>{t("invalid_number")}</p>
</div>
)}
</div>
)}
</div>
))}
{!eventType.disableGuests && guestToggle && (

View File

@ -40,6 +40,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
value: EventTypeCustomInputType.RADIO,
label: t("radio"),
},
{ value: EventTypeCustomInputType.PHONE, label: t("phone_number") },
];
const { selectedCustomInput } = props;

View File

@ -2,7 +2,6 @@ import { TFunction } from "next-i18next";
import { useRouter } from "next/router";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { useMemo, useState } from "react";
import { Loader } from "react-feather";
import { UseFormReturn } from "react-hook-form";
import { classNames } from "@calcom/lib";
@ -294,7 +293,7 @@ function EventTypeSingleLayout({
</Button>
</div>
}>
<ClientSuspense fallback={<Loader />}>
<ClientSuspense fallback={<Icon.FiLoader />}>
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
<div className="hidden xl:block">
<VerticalTabs

View File

@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.3.8",
"version": "2.4.0",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@ -121,7 +121,7 @@
"tailwindcss-radix": "^2.6.0",
"uuid": "^8.3.2",
"web3": "^1.7.5",
"zod": "^3.19.1"
"zod": "^3.20.2"
},
"devDependencies": {
"@babel/core": "^7.19.6",

View File

@ -24,7 +24,7 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
{
title: t("enable_apps"),
description: t("enable_apps_description"),
content: <AdminAppsList baseURL="/auth/setup" />,
content: <AdminAppsList baseURL="/auth/setup?step=2" />,
isLoading: false,
},
];
@ -32,7 +32,7 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
return (
<>
<main className="flex items-center bg-gray-100 print:h-full">
<WizardForm href="/auth/setup" steps={steps} disableNavigation={shouldDisable} />
<WizardForm href="/auth/setup" steps={steps} />
</main>
</>
);

View File

@ -1,18 +1,30 @@
import { useRouter } from "next/router";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon } from "@calcom/ui";
const StepDone = () => {
const router = useRouter();
const { t } = useLocale();
return (
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
<Icon.FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
<form
id="wizard-step-1"
name="wizard-step-1"
className="space-y-4"
onSubmit={(e) => {
e.preventDefault();
router.replace(`/auth/setup?step=2&category=calendar`);
}}>
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
<Icon.FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
</div>
<div className="max-w-[420px] text-center">
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
</div>
</div>
<div className="max-w-[420px] text-center">
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
</div>
</div>
</form>
);
};

View File

@ -174,74 +174,70 @@ export default function Availability({ schedule }: { schedule: number }) {
</Button>
</div>
}>
<div className="flex items-baseline sm:mt-0">
{/* TODO: Find a better way to guarantee alignment, but for now this'll do. */}
<Icon.FiArrowLeft className=" mr-3 text-transparent hover:cursor-pointer" />
<div className="w-full">
<Form
form={form}
id="availability-form"
handleSubmit={async ({ dateOverrides, ...values }) => {
updateMutation.mutate({
scheduleId: schedule,
dateOverrides: dateOverrides.flatMap((override) => override.ranges),
...values,
});
}}
className="-mx-4 flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
<div className="flex-1 divide-y divide-neutral-200 rounded-md border">
<div className=" py-5 pr-4 sm:p-6">
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">
{t("change_start_end")}
</h3>
{typeof me.data?.weekStart === "string" && (
<Schedule
control={control}
name="schedule"
weekStart={
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
me.data?.weekStart
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
}
/>
)}
</div>
{data?.workingHours && <DateOverride workingHours={data.workingHours} />}
<div className="w-full">
<Form
form={form}
id="availability-form"
handleSubmit={async ({ dateOverrides, ...values }) => {
updateMutation.mutate({
scheduleId: schedule,
dateOverrides: dateOverrides.flatMap((override) => override.ranges),
...values,
});
}}
className="flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
<div className="flex-1 divide-y divide-neutral-200 rounded-md border">
<div className=" py-5 sm:p-6">
<h3 className="mb-2 px-5 text-base font-medium leading-6 text-gray-900 sm:pl-0">
{t("change_start_end")}
</h3>
{typeof me.data?.weekStart === "string" && (
<Schedule
control={control}
name="schedule"
weekStart={
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
me.data?.weekStart
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
}
/>
)}
</div>
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
<div className="xl:max-w-80 mt-4 w-full pr-4 sm:p-0">
<div>
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
{t("timezone")}
</label>
<Controller
name="timeZone"
render={({ field: { onChange, value } }) =>
value ? (
<TimezoneSelect
value={value}
className="focus:border-brand mt-1 block w-72 rounded-md border-gray-300 text-sm"
onChange={(timezone) => onChange(timezone.value)}
/>
) : (
<SelectSkeletonLoader className="w-72" />
)
}
/>
</div>
<hr className="my-8" />
<div className="rounded-md">
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
<div className="mt-3 flex">
<Button href="/availability/troubleshoot" color="secondary">
{t("launch_troubleshooter")}
</Button>
</div>
{data?.workingHours && <DateOverride workingHours={data.workingHours} />}
</div>
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
<div className="xl:max-w-80 mt-4 w-full pr-4 sm:p-0">
<div>
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
{t("timezone")}
</label>
<Controller
name="timeZone"
render={({ field: { onChange, value } }) =>
value ? (
<TimezoneSelect
value={value}
className="focus:border-brand mt-1 block w-72 rounded-md border-gray-300 text-sm"
onChange={(timezone) => onChange(timezone.value)}
/>
) : (
<SelectSkeletonLoader className="w-72" />
)
}
/>
</div>
<hr className="my-8" />
<div className="rounded-md">
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
<div className="mt-3 flex">
<Button href="/availability/troubleshoot" color="secondary">
{t("launch_troubleshooter")}
</Button>
</div>
</div>
</div>
</Form>
</div>
</div>
</Form>
</div>
</Shell>
);

View File

@ -6,6 +6,7 @@ import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Alert, Button, EmailField, PasswordField, TextField } from "@calcom/ui";
import { HeadSeo } from "@calcom/web/components/seo/head-seo";
@ -26,6 +27,8 @@ type FormValues = {
export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const router = useRouter();
const telemetry = useTelemetry();
const methods = useForm<FormValues>({
defaultValues: prepopulateFormValues,
});
@ -53,8 +56,12 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
})
.then(handleErrors)
.then(async () => {
await signIn("Cal.com", {
callbackUrl: router.query.callbackUrl ? `${WEBAPP_URL}/${router.query.callbackUrl}` : WEBAPP_URL,
telemetry.event(telemetryEventTypes.login, collectPageParameters());
await signIn<"credentials">("credentials", {
...data,
callbackUrl: router.query.callbackUrl
? `${WEBAPP_URL}/${router.query.callbackUrl}`
: `${WEBAPP_URL}/getting-started`,
});
})
.catch((err) => {
@ -111,7 +118,9 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
className="w-5/12 justify-center"
onClick={() =>
signIn("Cal.com", {
callbackUrl: (`${WEBAPP_URL}/${router.query.callbackUrl}` || "") as string,
callbackUrl: router.query.callbackUrl
? `${WEBAPP_URL}/${router.query.callbackUrl}`
: `${WEBAPP_URL}/getting-started`,
})
}>
{t("login_instead")}

View File

@ -126,9 +126,9 @@
"no_data_yet": "Noch keine Daten",
"ping_test": "Pingtest",
"add_to_homescreen": "Fügen Sie diese App Ihrem Startbildschirm für schnelleren Zugriff hinzu.",
"upcoming": "Bevorstehend",
"recurring": "Wiederkehrend",
"past": "Vergangen",
"upcoming": "Bevorstehende",
"recurring": "Wiederkehrende",
"past": "Vergangene",
"choose_a_file": "Datei auswählen...",
"upload_image": "Bild hochladen",
"upload_target": "{{target}} hochladen",
@ -240,8 +240,8 @@
"add_another_calendar": "Einen weiteren Kalender hinzufügen",
"other": "Sonstige",
"emailed_you_and_attendees": "Wir haben Ihnen und den anderen Teilnehmern eine Einladung zum Kalender mit allen Details zugeschickt.",
"emailed_you_and_attendees_recurring": "Wir haben Ihnn und den anderen Teilnemern eine Kalendereinladung für den ersten Termin dieser wiederkehrenden Termins gesendet.",
"emailed_you_and_any_other_attendees": "Wir haben Ihnen und allen anderen Teilnehmer diesen Informationen per E-Mail geschickt.",
"emailed_you_and_attendees_recurring": "Wir haben Ihnen und den anderen Teilnemern eine Kalendereinladung für den ersten Termin dieser wiederkehrenden Termins gesendet.",
"emailed_you_and_any_other_attendees": "Wir haben Ihnen und allen anderen Teilnehmern diese Informationen per E-Mail zugeschickt.",
"needs_to_be_confirmed_or_rejected": "Ihre Buchung muss noch bestätigt oder abgelehnt werden.",
"needs_to_be_confirmed_or_rejected_recurring": "Ihr wiederkehrender Termin muss noch bestätigt oder abgelehnt werden.",
"user_needs_to_confirm_or_reject_booking": "{{user}} muss den Termin noch bestätigen oder ablehnen.",
@ -252,7 +252,7 @@
"submitted_recurring": "Ihr wiederkehrender Termin wurde gebucht",
"booking_submitted": "Ihre Buchung wurde versandt",
"booking_submitted_recurring": "Ihre wiederkehrende Buchung wurde versandt",
"booking_confirmed": "Ihre Termin wurde bestätigt",
"booking_confirmed": "Ihr Termin wurde bestätigt",
"booking_confirmed_recurring": "Ihr wiederkehrender Termin wurde bestätigt",
"warning_recurring_event_payment": "Zahlungen werden bei wiederkehrenden Terminen noch nicht unterstützt",
"warning_payment_recurring_event": "Zahlungen werden bei wiederkehrenden Terminen noch nicht unterstützt",
@ -282,7 +282,7 @@
"error_end_time_next_day": "Die Endzeit darf nicht größer als 24 Stunden sein",
"back_to_bookings": "Zurück zu den Buchungen",
"free_to_pick_another_event_type": "Sie können jederzeit einen anderen Termin wählen.",
"cancelled": "Abgesagt",
"cancelled": "Abgesagte",
"cancellation_successful": "Stornierung erfolgreich",
"really_cancel_booking": "Möchten Sie wirklich stornieren?",
"cannot_cancel_booking": "Sie können diese Buchung nicht stornieren",
@ -303,7 +303,7 @@
"bookings": "Buchungen",
"bookings_description": "Sehen Sie anstehende und vergangene Veranstaltungen, die über Ihre Events gebucht wurden.",
"upcoming_bookings": "Sobald jemand eine Zeit bei Ihnen bucht können Sie das hier sehen.",
"recurring_bookings": "Sobald jemand einen wiederkehrenden Termin mit Ihnen bucht er hier erscheinen.",
"recurring_bookings": "Sobald jemand einen wiederkehrenden Termin mit Ihnen bucht wird er hier erscheinen.",
"past_bookings": "Ihre früheren Buchungen werden hier angezeigt.",
"cancelled_bookings": "Ihre stornierten Buchungen werden hier angezeigt.",
"unconfirmed_bookings": "Ihre unbestätigten Buchungen werden hier angezeigt.",

View File

@ -793,7 +793,7 @@
"no_category_apps_description_analytics": "Add an analytics app for your booking pages",
"no_category_apps_description_automation": "Add an automation app to use",
"no_category_apps_description_other": "Add any other type of app to do all sorts of things",
"installed_app_calendar_description": "Set the calendar(s) to check for conflicts to prevent double bookings.",
"installed_app_calendar_description": "Set the calendars to check for conflicts to prevent double bookings.",
"installed_app_conferencing_description": "Add your favourite video conferencing apps for your meetings",
"installed_app_payment_description": "Configure which payment processing services to use when charging your clients.",
"installed_app_analytics_description": "Configure which analytics apps to use for your booking pages",
@ -1215,7 +1215,7 @@
"event_time_info": "The event start time",
"location_info": "The event location",
"organizer_name_info": "Your name",
"additional_notes_info": "The Additional notes of booking",
"additional_notes_info": "The additional notes of booking",
"attendee_name_info": "The person booking's name",
"to": "To",
"workflow_turned_on_successfully": "{{workflowName}} workflow turned {{offOn}} successfully",
@ -1444,6 +1444,8 @@
"individual":"Individual",
"all_bookings_filter_label":"All Bookings",
"all_users_filter_label":"All Users",
"meeting_url_workflow": "Meeting url",
"meeting_url_info": "The event meeting conference url",
"date_overrides": "Date overrides",
"date_overrides_subtitle": "Add dates when your availability changes from your daily hours.",
"date_overrides_info": "Date overrides are archived automatically after the date has passed",

View File

@ -1400,5 +1400,11 @@
"test_routing": "Tester le routage",
"payment_app_disabled": "Un administrateur a désactivé une application de paiement",
"edit_event_type": "Modifier le type d'événement",
"collective_scheduling": "Programmation collective",
"make_it_easy_to_book": "Il est facile de réserver votre équipe quand tout le monde sera disponible.",
"find_the_best_person": "Trouvez la meilleure personne disponible et passez en revue votre équipe.",
"fixed_round_robin": "Round robin établi",
"add_one_fixed_attendee": "Ajoutez un participant fixe et un round robin via un certain nombre de participants.",
"calcom_is_better_with_team": "Cal.com est meilleur avec des équipes",
"admin_apps_description": "Activer les applications pour votre instance de Cal"
}

View File

@ -52,6 +52,7 @@
"still_waiting_for_approval": "Er is nog een openstaande goedkeuring voor een evenement",
"event_is_still_waiting": "Openstaand verzoek voor evenement: {{attendeeName}} - {{date}} - {{eventType}}",
"no_more_results": "Geen verdere resultaten",
"no_results": "Geen resultaten",
"load_more_results": "Meer resultaten laden",
"integration_meeting_id": "{{integrationName}} afspraak ID: {{meetingId}}",
"confirmed_event_type_subject": "Bevestigd: {{eventType}} met {{name}} op {{date}}",
@ -248,6 +249,7 @@
"add_to_calendar": "Toevoegen aan kalender",
"add_another_calendar": "Andere agenda toevoegen",
"other": "Overige",
"email_sign_in_subject": "Uw aanmeldingslink voor {{appName}}",
"emailed_you_and_attendees": "We hebben u en de andere deelnemende een kalender uitnodiging met alle details gestuurd.",
"emailed_you_and_attendees_recurring": "We hebben u en de andere deelnemers een agenda-uitnodiging voor de eerste van deze terugkerende gebeurtenissen gestuurd.",
"emailed_you_and_any_other_attendees": "U en alle andere deelnemers zijn gemaild met deze informatie.",
@ -419,6 +421,7 @@
"current_incorrect_password": "Uw wachtwoord is verkeerd",
"password_hint_caplow": "Mix van hoofd- en kleine letters",
"password_hint_min": "Minimaal 8 tekens lang",
"password_hint_admin_min": "Minimaal 15 tekens lang",
"password_hint_num": "Minimaal 1 cijfer bevatten",
"invalid_password_hint": "Het wachtwoord moet minimaal 7 tekens lang zijn, minimaal 1 cijfer bevatten en bestaan uit een mix van hoofd- en kleine letters",
"incorrect_password": "Uw wachtwoord is incorrect.",
@ -462,11 +465,14 @@
"booking_confirmation": "Bevestig uw {{eventTypeTitle}} met {{profileName}}",
"booking_reschedule_confirmation": "Opnieuw plannen van uw {{eventTypeTitle}} met {{profileName}}",
"in_person_meeting": "Online of persoonlijk afspreken",
"attendeeInPerson": "Persoonlijk (adres deelnemer)",
"inPerson": "Persoonlijk (adres organisator)",
"link_meeting": "Vergadering koppelen",
"phone_call": "Telefoonnummer deelnemer",
"your_number": "Uw telefoonnummer",
"phone_number": "Telefoon nummer",
"attendee_phone_number": "Telefoonnummer deelnemer",
"organizer_phone_number": "Telefoonnummer organisator",
"host_phone_number": "Uw telefoonnummer",
"enter_phone_number": "Telefoonnummer",
"reschedule": "Boeking wijzigen",
@ -553,6 +559,10 @@
"collective": "Collectief",
"collective_description": "Plan vergaderingen wanneer alle geselecteerde teamleden beschikbaar zijn.",
"duration": "Looptijd",
"available_durations": "Beschikbare duur",
"default_duration": "Standaardduur",
"default_duration_no_options": "Kies eerst de beschikbare duur",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"minutes": "Minuten",
"round_robin": "Round Robin",
"round_robin_description": "Afspraken wisselen tussen meerdere teamleden.",
@ -618,6 +628,7 @@
"teams": "Teams",
"team": "Team",
"team_billing": "Teamfacturatie",
"team_billing_description": "Beheer de facturering voor uw team",
"upgrade_to_flexible_pro_title": "We hebben het factureren gewijzigd voor teams",
"upgrade_to_flexible_pro_message": "Er zijn leden in uw team zonder een plaats. Upgrade uw Pro-abonnement om de ontbrekende plaatsen te dekken.",
"changed_team_billing_info": "Vanaf januari 2022 rekenen we per plaats voor teamleden. Leden van uw team die Pro gratis hadden, hebben nu een proefperiode van 14 dagen. Zodra hun proefperiode afloopt, worden deze leden verborgen voor uw team, tenzij u nu upgradet.",
@ -693,6 +704,7 @@
"hide_event_type": "Verberg evenement",
"edit_location": "Locatie wijzigen",
"into_the_future": "in de toekomst",
"when_booked_with_less_than_notice": "Wanneer geboekt met minder dan <time></time> kennisgeving",
"within_date_range": "Binnen een datumbereik",
"indefinitely_into_future": "Voor onbepaalde tijd",
"add_new_custom_input_field": "Nieuw invoerveld toevoegen",
@ -712,6 +724,7 @@
"delete_account_confirmation_message": "Weet u zeker dat u uw {{appName}}-account wilt verwijderen? Iedereen met wie u uw accountlink gedeeld heeft, kan er niet meer mee boeken en alle voorkeuren die u opgeslagen heeft gaan verloren.",
"integrations": "Integraties",
"apps": "Apps",
"apps_listing": "App-beschrijving",
"category_apps": "apps voor {{category}}",
"app_store": "App Store",
"app_store_description": "Mensen, technologie en de werkplek verbinden.",
@ -735,6 +748,7 @@
"toggle_calendars_conflict": "Schakel de agenda's in die u wilt controleren op conflicten om dubbele boekingen te voorkomen.",
"select_destination_calendar": "Maak gebeurtenissen aan op",
"connect_additional_calendar": "Extra agenda kopppelen",
"calendar_updated_successfully": "Agenda bijgewerkt",
"conferencing": "Confereren",
"calendar": "Agenda",
"payments": "Betalingen",
@ -767,6 +781,7 @@
"trending_apps": "Trending apps",
"explore_apps": "{{category}}-apps",
"installed_apps": "Geinstalleerde apps",
"free_to_use_apps": "Gratis",
"no_category_apps": "Geen apps voor {{category}}",
"no_category_apps_description_calendar": "Voeg een agenda-app toe om te controleren op conflicten om dubbele boekingen te voorkomen",
"no_category_apps_description_conferencing": "Probeer een conferentieapp toe te voegen om videogesprekken met uw klanten te integreren",
@ -805,6 +820,8 @@
"verify_wallet": "Wallet verifiëren",
"connect_metamask": "Metamasker verbinden",
"create_events_on": "Maak gebeurtenissen aan in de",
"enterprise_license": "Dit is een bedrijfsfunctie",
"enterprise_license_description": "Om deze functie in te schakelen, krijgt u een implementatiesleutel op de {{consoleUrl}}-console en voegt u deze toe aan uw .env als CALCOM_LICENSE_KEY. Als uw team al een licentie heeft, neem dan contact op met {{supportMail}} voor hulp.",
"missing_license": "Ontbrekende licentie",
"signup_requires": "Commerciële licentie vereist",
"signup_requires_description": "{{companyName}} biedt momenteel geen gratis opensourceversie van de registratiepagina aan. Om volledige toegang te krijgen tot de registratieonderdelen moet u een commerciële licentie verkrijgen. Voor persoonlijk gebruik raden we aan om accounts aan te maken op het Prisma Data Platform of een andere Postgres-interface.",
@ -896,6 +913,7 @@
"user_impersonation_heading": "Imitatie van gebruiker",
"user_impersonation_description": "Hiermee kan ons ondersteuningsteam tijdelijk als u inloggen zodat we eventuele problemen die u aan ons meldt snel op kunnen lossen.",
"team_impersonation_description": "Hiermee kunnen uw teamleden zich tijdelijk aanmelden als u.",
"allow_booker_to_select_duration": "Booker toestaan om een duur te selecteren",
"impersonate_user_tip": "Alle gebruik van deze functie wordt gecontroleerd.",
"impersonating_user_warning": "Gebruikersnaam \"{{user}}\" wordt geïmiteerd.",
"impersonating_stop_instructions": "<0>Klik hier om te stoppen</0>.",
@ -1013,6 +1031,9 @@
"error_removing_app": "Fout bij het verwijderen van de app",
"web_conference": "Webconferentie",
"requires_confirmation": "Vereist bevestiging",
"always_requires_confirmation": "Altijd",
"requires_confirmation_threshold": "Vereist bevestiging als geboekt met < {{time}} $t({{unit}}_timeUnit) kennisgeving",
"may_require_confirmation": "Kan bevestiging vereisen",
"nr_event_type_one": "{{count}} gebeurtenistype",
"nr_event_type_other": "{{count}} gebeurtenistypen",
"add_action": "Actie toevoegen",
@ -1100,6 +1121,9 @@
"event_limit_tab_description": "Hoe vaak u geboekt kunt worden",
"event_advanced_tab_description": "Agenda-instellingen en meer...",
"event_advanced_tab_title": "Geavanceerd",
"event_setup_multiple_duration_error": "Gebeurtenisconfiguratie: voor meerdere duren is minimaal 1 optie vereist.",
"event_setup_multiple_duration_default_error": "Gebeurtenisconfiguratie: selecteer een geldige standaardduur.",
"event_setup_booking_limits_error": "Reserveringslimieten moeten in oplopende volgorde zijn. [dag, week, maand, jaar]",
"select_which_cal": "Selecteer aan welke agenda u boekingen wilt toevoegen",
"custom_event_name": "Aangepaste gebeurtenisnaam",
"custom_event_name_description": "Maak aangepaste gebeurtenisnamen om weer te geven op agendagebeurtenissen",
@ -1153,8 +1177,11 @@
"invoices": "Facturen",
"embeds": "Ingesloten opties",
"impersonation": "Imitatie",
"impersonation_description": "Instellingen voor imitatie van gebruiker beheren",
"users": "Gebruikers",
"profile_description": "Beheer de instellingen van uw {{appName}}-profiel",
"users_description": "Hier vindt u een lijst met alle gebruikers",
"users_listing": "Gebruikersbeschrijving",
"general_description": "Beheer de instellingen voor uw taal en tijdzone",
"calendars_description": "Configureer hoe uw typen gebeurtenissen samenwerken met uw agenda's",
"appearance_description": "Beheer de instellingen voor uw boekingsweergave",
@ -1359,6 +1386,7 @@
"number_sms_notifications": "Telefoonnummer (sms-meldingen)",
"attendee_email_workflow": "E-mailadres deelnemer",
"attendee_email_info": "Het e-mailadres van de persoon die boekt",
"kbar_search_placeholder": "Typ een opdracht of zoek...",
"invalid_credential": "Oh nee! Het lijkt erop dat de machtiging is verlopen of is ingetrokken. Installeer het opnieuw.",
"choose_common_schedule_team_event": "Kies een gemeenschappelijke planning",
"choose_common_schedule_team_event_description": "Schakel dit in als u een gemeenschappelijke planning tussen organisatoren wilt gebruiken. Indien uitgeschakeld, wordt elke organisator geboekt op basis van zijn standaardplanning.",
@ -1369,5 +1397,43 @@
"test_preview": "Voorbeeld testen",
"route_to": "Routeren naar",
"test_preview_description": "Test uw routeringsformulier zonder gegevens te versturen",
"test_routing": "Routering testen"
"test_routing": "Routering testen",
"payment_app_disabled": "Een beheerder heeft een betaalapp uitgeschakeld",
"edit_event_type": "Gebeurtenistype bewerken",
"collective_scheduling": "Collectieve planning",
"make_it_easy_to_book": "Maak het gemakkelijk om uw team te boeken wanneer iedereen beschikbaar is.",
"find_the_best_person": "Zoek de beste beschikbare persoon en blader door uw team.",
"fixed_round_robin": "Vaste round robin",
"add_one_fixed_attendee": "Voeg één vaste deelnemer en round robin toe via een aantal deelnemers.",
"calcom_is_better_with_team": "Cal.com is beter met teams",
"add_your_team_members": "Voeg uw teamleden toe aan uw gebeurtenistypes. Gebruik collectieve planning om iedereen op te nemen of zoek de meest geschikte persoon met round robin-planning.",
"booking_limit_reached": "Boekingslimiet voor dit gebeurtenistype is bereikt",
"admin_has_disabled": "Een beheerder heeft {{appName}} uitgeschakeld",
"disabled_app_affects_event_type": "Een beheerder heeft {{appName}} uitgeschakeld die van invloed is op uw gebeurtenistype {{eventType}}",
"disable_payment_app": "De beheerder heeft {{appName}} uitgeschakeld die van invloed is op uw gebeurtenistype {{title}}. Deelnemers kunnen dit tyoe gebeurtenissen nog steeds boeken, maar worden niet gevraagd te betalen. U kunt het gebeurtenistype verbergen om dit te voorkomen totdat uw beheerde de betaalmethode opnieuw inschakelt.",
"payment_disabled_still_able_to_book": "Deelnemers kunnen dit type gebeurtenis nog steeds boeken, maar worden niet gevraagd te betalen. U kunt het gebeurtenistype verbergen om dit te voorkomen totdat uw beheerde de betaalmethode opnieuw inschakelt.",
"app_disabled_with_event_type": "De beheerder heeft {{appName}} uitgeschakeld die van invloed is op uw gebeurtenistype {{title}}.",
"app_disabled_video": "De beheerde heeft {{appName}} uitgeschakeld, wat van invloed kan zijn op uw gebeurtenistypes. Als u gebeurtenistypes heeft met {{appName}} als locatie, is het standaard Cal Video.",
"app_disabled_subject": "{{appName}} is uitgeschakeld",
"navigate_installed_apps": "Naar geïnstalleerde apps",
"disabled_calendar": "Als u een andere agenda heeft geïnstalleerd, worden nieuwe boekingen daaraan toegevoegd. Zo niet, koppel dan een nieuwe agenda zodat u geen nieuwe boekingen mist.",
"enable_apps": "Apps inschakelen",
"enable_apps_description": "Schakel apps in die gebruikers kunnen integreren met Cal.com",
"app_is_enabled": "{{appName}} is ingeschakeld",
"app_is_disabled": "{{appName}} is uitgeschakeld",
"keys_have_been_saved": "Sleutels zijn opgeslagen",
"disable_app": "App uitschakelen",
"disable_app_description": "Het uitschakelen van deze app kan problemen veroorzaken met de interactie tussen uw gebruikers en Cal",
"edit_keys": "Sleutels bewerken",
"no_available_apps": "Er zijn geen beschikbare apps",
"no_available_apps_description": "Zorg ervoor dat er apps zijn in uw implementatie onder \"packages/app-store\"",
"no_apps": "Er zijn geen apps ingeschakeld in dit exemplaar van Cal",
"apps_settings": "Apps-instellingen",
"fill_this_field": "Vul dit veld in",
"options": "Opties",
"enter_option": "Voer optie {{index}} in",
"add_an_option": "Voeg een optie toe",
"radio": "Radio",
"event_type_duplicate_copy_text": "{{slug}}-kopie",
"set_as_default": "Als standaard instellen"
}

View File

@ -1401,5 +1401,7 @@
"payment_app_disabled": "Một quản trị viên đã vô hiệu hoá một ứng dụng thanh toán",
"edit_event_type": "Sửa loại sự kiện",
"collective_scheduling": "Lên lịch tập thể",
"make_it_easy_to_book": "Tạo điều kiện dễ dàng đặt lịch cho nhóm của bạn khi mọi người đều rảnh.",
"find_the_best_person": "Tìm người tốt nhất có được và xoay vòng trong nhóm bạn.",
"admin_apps_description": "Bật các ứng dụng cho thực thể Cal của bạn"
}

View File

@ -704,6 +704,7 @@
"hide_event_type": "隐藏活动类型",
"edit_location": "编辑位置",
"into_the_future": "未来的时间",
"when_booked_with_less_than_notice": "以少于 <time></time> 的通知提前时间预约后",
"within_date_range": "在日期范围内",
"indefinitely_into_future": "未来无限时间",
"add_new_custom_input_field": "新增自定义输入字段",
@ -1032,6 +1033,7 @@
"web_conference": "网络会议",
"requires_confirmation": "需要确认",
"always_requires_confirmation": "始终",
"requires_confirmation_threshold": "如果预约通知提前时间少于 {{time}}$t({{unit}}_timeUnit),则需要确认",
"may_require_confirmation": "可能需要确认",
"nr_event_type_one": "{{count}} 种活动类型",
"nr_event_type_other": "{{count}} 种活动类型",
@ -1400,8 +1402,13 @@
"payment_app_disabled": "管理员已禁用支付应用",
"edit_event_type": "编辑活动类型",
"collective_scheduling": "集体日程安排",
"make_it_easy_to_book": "当每个人都可预约时,可轻松预约您的团队。",
"find_the_best_person": "查找最合适的人选,在您的团队内轮流选择。",
"fixed_round_robin": "固定轮流模式",
"add_one_fixed_attendee": "添加一名固定参与者,在多名参与者中轮流选择。",
"calcom_is_better_with_team": "Cal.com 更适合团队",
"add_your_team_members": "将您的团队成员添加到您的活动类型。使用集体日程安排来包括所有人或以轮流安排的方式找到最适合的人员。",
"booking_limit_reached": "已达到此活动类型的预约限制",
"admin_has_disabled": "管理员已禁用 {{appName}}",
"disabled_app_affects_event_type": "管理员已禁用会影响活动类型 {{eventType}} 的 {{appName}}",
"disable_payment_app": "管理员已禁用会影响活动类型 {{title}} 的 {{appName}}。参与者仍然能预约此类型的活动,但不会被提示付款。您可以隐藏该活动类型来防止出现这种情况,直到管理员重新启用您的支付方式。",
@ -1410,6 +1417,7 @@
"app_disabled_video": "管理员已禁用可能会影响活动类型的 {{appName}}。如果您有将 {{appName}} 作为位置的活动类型,则默认为 Cal Video。",
"app_disabled_subject": "{{appName}} 已被禁用",
"navigate_installed_apps": "转到已安装的应用",
"disabled_calendar": "如果您安装了其他日历,新预约将添加至其中。如果没有,则会连接一个新日历,这样您就不会错过任何新预约。",
"enable_apps": "启用应用",
"enable_apps_description": "启用用户可以与 Cal.com 集成的应用",
"app_is_enabled": "{{appName}} 已启用",
@ -1427,5 +1435,7 @@
"options": "选项",
"enter_option": "输入选项 {{index}}",
"add_an_option": "添加选项",
"radio": "无线电",
"event_type_duplicate_copy_text": "{{slug}}-副本",
"set_as_default": "设置为默认"
}

View File

@ -1,3 +1,4 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useMemo } from "react";
import { classNames } from "@calcom/lib";
@ -10,23 +11,33 @@ const AppCategoryNavigation = ({
children,
containerClassname,
className,
fromAdmin,
}: {
baseURL: string;
children: React.ReactNode;
containerClassname: string;
className?: string;
fromAdmin?: boolean;
}) => {
const appCategories = useMemo(() => getAppCategories(baseURL), [baseURL]);
const [animationRef] = useAutoAnimate<HTMLDivElement>();
const appCategories = useMemo(() => getAppCategories(baseURL, fromAdmin), [baseURL, fromAdmin]);
return (
<div className={classNames("flex flex-col p-2 md:p-0 xl:flex-row", className)}>
<div className="hidden xl:block">
<VerticalTabs tabs={appCategories} sticky linkProps={{ shallow: true }} />
<VerticalTabs
tabs={appCategories}
sticky
linkProps={{ shallow: true }}
itemClassname={classNames(fromAdmin && "w-60")}
/>
</div>
<div className="block overflow-x-scroll xl:hidden">
<div className="mb-4 block overflow-x-scroll xl:hidden">
<HorizontalTabs tabs={appCategories} linkProps={{ shallow: true }} />
</div>
<main className={containerClassname}>{children}</main>
<main className={containerClassname} ref={animationRef}>
{children}
</main>
</div>
);
};

View File

@ -1,40 +1,44 @@
import { Icon } from "@calcom/ui";
const getAppCategories = (baseURL: string) => {
function getHref(baseURL: string, category: string, useQueryParam: boolean) {
return useQueryParam ? `${baseURL}&category=${category}` : `${baseURL}/${category}`;
}
const getAppCategories = (baseURL: string, useQueryParam = false) => {
return [
{
name: "calendar",
href: `${baseURL}/calendar`,
href: getHref(baseURL, "calendar", useQueryParam),
icon: Icon.FiCalendar,
},
{
name: "conferencing",
href: `${baseURL}/conferencing`,
href: getHref(baseURL, "conferencing", useQueryParam),
icon: Icon.FiVideo,
},
{
name: "payment",
href: `${baseURL}/payment`,
href: getHref(baseURL, "payment", useQueryParam),
icon: Icon.FiCreditCard,
},
{
name: "automation",
href: `${baseURL}/automation`,
href: getHref(baseURL, "automation", useQueryParam),
icon: Icon.FiShare2,
},
{
name: "analytics",
href: `${baseURL}/analytics`,
href: getHref(baseURL, "analytics", useQueryParam),
icon: Icon.FiBarChart,
},
{
name: "web3",
href: `${baseURL}/web3`,
href: getHref(baseURL, "web3", useQueryParam),
icon: Icon.FiBarChart,
},
{
name: "other",
href: `${baseURL}/other`,
href: getHref(baseURL, "other", useQueryParam),
icon: Icon.FiGrid,
},
];

View File

@ -0,0 +1,18 @@
---
items:
- /api/app-store/amie/1.jpg
- /api/app-store/amie/2.jpg
- /api/app-store/amie/3.jpg
---
<iframe class="w-full aspect-video -mx-2" width="560" height="315" src="https://www.youtube.com/embed/OGe1NYKhZE8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
## The joyful productivity app
- Use your calendar as a todo list
- Color your calendar to organize
- Instantly know if someone is available
- Track what you listened to when
- Send scheduling links guests love
- Always know what your team is up to

View File

@ -0,0 +1,10 @@
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
...config,
} as AppMeta;
export default metadata;

View File

@ -0,0 +1,21 @@
import { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import { createDefaultInstallation } from "../../_utils/installation";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
// Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
redirect: {
newTab: true,
url: "https://amie.so/signup",
},
createCredential: ({ appType, user, slug }) =>
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
};
export default handler;

View File

@ -0,0 +1 @@
export { default as add } from "./add";

View File

@ -0,0 +1,16 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Amie",
"slug": "amie",
"type": "amie_other",
"imageSrc": "/api/app-store/amie/icon.svg",
"logo": "/api/app-store/amie/icon.svg",
"url": "https://cal.com/apps/amie",
"variant": "other",
"categories": ["calendar"],
"publisher": "Cal.com, Inc.",
"email": "support@cal.com",
"description": "The joyful productivity app\r\r",
"extendsFeature": "User",
"__createdUsingCli": true
}

View File

@ -0,0 +1,2 @@
export * as api from "./api";
export { metadata } from "./_metadata";

View File

@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/amie",
"version": "0.0.0",
"main": "./index.ts",
"description": "The joyful productivity app\r\r",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

View File

@ -0,0 +1,4 @@
<svg width="90" height="90" viewBox="0 0 90 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="90" height="90" rx="8" fill="#FCBABC"/>
<path d="M46.3736 43.578C47.8926 43.578 48.6956 44.603 48.6956 46.544V54.119C48.6956 54.378 48.9036 54.587 49.1606 54.587H51.2286C51.4856 54.587 51.6936 54.378 51.6936 54.119V45.631C51.6936 44.197 51.2656 42.975 50.4536 42.103C49.6376 41.225 48.4946 40.762 47.1456 40.762C45.5216 40.762 44.1576 41.378 42.9726 42.647L42.9296 42.694L42.8956 42.641C42.1116 41.413 40.8836 40.762 39.3496 40.762C37.8336 40.762 36.5966 41.334 35.6696 42.462L35.5826 42.569V41.491C35.5826 41.231 35.3746 41.022 35.1176 41.022H33.0496C32.7926 41.022 32.5846 41.231 32.5846 41.491V54.116C32.5846 54.375 32.7926 54.584 33.0496 54.584H35.1176C35.3746 54.584 35.5826 54.375 35.5826 54.116V46.619C35.5826 45.456 36.0536 44.747 36.4476 44.353C36.9476 43.858 37.6186 43.579 38.3196 43.575C39.8386 43.575 40.6416 44.6 40.6416 46.541V54.116C40.6416 54.375 40.8496 54.584 41.1066 54.584H43.1746C43.4316 54.584 43.6396 54.375 43.6396 54.116V46.619C43.6396 45.456 44.1106 44.747 44.5046 44.353C45.0006 43.862 45.6826 43.578 46.3736 43.578ZM74.9066 47.453C74.9066 45.575 74.3636 43.891 73.3806 42.709C72.3206 41.434 70.7836 40.762 68.9386 40.762C67.1436 40.762 65.5096 41.484 64.3356 42.797C63.1726 44.094 62.5336 45.878 62.5336 47.819C62.5336 51.972 65.2216 54.875 69.0686 54.875C71.5276 54.875 73.5546 53.662 74.5966 51.606C74.7266 51.347 74.6146 51.053 74.3446 50.95L72.6806 50.316C72.4506 50.228 72.1966 50.337 72.0946 50.562C71.5766 51.675 70.4796 52.322 69.0716 52.322C67.0446 52.322 65.6406 50.859 65.4076 48.503L65.4016 48.447H74.4446C74.7016 48.447 74.9096 48.237 74.9096 47.978V47.453H74.9066ZM65.5446 46.203L65.5596 46.141C66.0156 44.331 67.2366 43.291 68.9136 43.291C70.6866 43.291 71.8526 44.413 71.8806 46.15V46.2H65.5446V46.203ZM30.1976 53.953L23.5636 36.303C23.4956 36.122 23.3226 36 23.1296 36H20.0986C19.9056 36 19.7326 36.122 19.6646 36.303L13.0306 53.953C12.9156 54.259 13.1386 54.587 13.4646 54.587H15.6406C15.8356 54.587 16.0096 54.466 16.0776 54.281L17.2646 51.072H25.8916L27.0976 54.284C27.1666 54.466 27.3396 54.587 27.5316 54.587H29.7606C29.9136 54.588 30.0576 54.513 30.1446 54.386C30.2326 54.259 30.2516 54.097 30.1976 53.953ZM18.3066 48.259L21.5486 39.503L24.8376 48.259H18.3066ZM53.7796 41.494V43.362C53.7796 43.622 53.9876 43.831 54.2446 43.831H56.5386V54.119C56.5386 54.378 56.7466 54.588 57.0036 54.588H59.0716C59.3286 54.588 59.5366 54.378 59.5366 54.119V41.494C59.5366 41.234 59.3286 41.025 59.0716 41.025H54.2416C53.9876 41.025 53.7796 41.234 53.7796 41.494ZM56.4886 36.469V38.631C56.4886 38.891 56.6966 39.1 56.9536 39.1H59.1276C59.3846 39.1 59.5926 38.891 59.5926 38.631V36.469C59.5926 36.209 59.3846 36 59.1276 36H56.9536C56.6966 36 56.4886 36.212 56.4886 36.469Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -2,6 +2,7 @@
This file is autogenerated using the command `yarn app-store:build --watch`.
Don't modify this file manually.
**/
import { metadata as amie_meta } from "./amie/_metadata";
import { metadata as applecalendar_meta } from "./applecalendar/_metadata";
import { metadata as around_meta } from "./around/_metadata";
import { metadata as caldavcalendar_meta } from "./caldavcalendar/_metadata";
@ -50,6 +51,7 @@ import { metadata as zapier_meta } from "./zapier/_metadata";
import { metadata as zoomvideo_meta } from "./zoomvideo/_metadata";
export const appStoreMetadata = {
amie: amie_meta,
applecalendar: applecalendar_meta,
around: around_meta,
caldavcalendar: caldavcalendar_meta,

View File

@ -3,6 +3,7 @@
Don't modify this file manually.
**/
export const apiHandlers = {
amie: import("./amie/api"),
applecalendar: import("./applecalendar/api"),
around: import("./around/api"),
caldavcalendar: import("./caldavcalendar/api"),

View File

@ -1,13 +1,12 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { Check, X } from "react-feather";
import { Controller, useForm } from "react-hook-form";
import { Toaster } from "react-hot-toast";
import z from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Form, showToast, TextField } from "@calcom/ui";
import { Button, Form, showToast, TextField, Icon } from "@calcom/ui";
const formSchema = z.object({
api_key: z.string(),
@ -107,7 +106,7 @@ export default function CloseComSetup() {
type="submit"
loading={testLoading}
disabled={testPassed === true}
StartIcon={testPassed !== undefined ? (testPassed ? Check : X) : undefined}
StartIcon={testPassed !== undefined ? (testPassed ? Icon.FiCheck : Icon.FiX) : undefined}
className={
testPassed !== undefined
? testPassed

View File

@ -21,7 +21,7 @@
"@stripe/stripe-js": "^1.35.0",
"stripe": "^9.16.0",
"uuid": "^8.3.2",
"zod": "^3.19.1"
"zod": "^3.20.2"
},
"devDependencies": {
"@calcom/types": "*",

View File

@ -18,7 +18,7 @@ yarn dev
## Running Tests
Ensure that the main App is running on port 3000 (e.g. yarn dx) already and then run the following command:
Ensure that the main App is running on port 3000 (e.g. yarn dx) already. Also ensure dev server for embed-core is running and then run the following command:
Start the server on 3100 port
```bash
@ -31,6 +31,8 @@ And from another terminal you can run the following command to execute tests:
yarn embed-tests-quick
```
Note: `getEmbedIframe` and `addEmbedListeners` work as a team but they only support opening up embed in a fresh load. Opening an embed closing it and then opening another embed isn't supported yet.
## Shipping to Production
```bash

View File

@ -97,7 +97,10 @@
<button data-cal-namespace="popupTeamLinkLightTheme" data-cal-config='{"theme":"light"}' data-cal-link="team/seeded-team/collective-seeded-team-event">Book with Test Team[Light Theme]</button>
<button data-cal-namespace="popupTeamLinkDarkTheme" data-cal-config='{"theme":"dark"}' data-cal-link="team/seeded-team/collective-seeded-team-event">Book with Test Team[Dark Theme]</button>
<button data-cal-namespace="popupTeamLinksList" data-cal-link="team/seeded-team/">See Team Links [Auto Theme]</button>
<button data-cal-namespace="popupReschedule" data-cal-link="reschedule/qm3kwt3aTnVD7vmP9tiT2f">Reschedule Event[Auto Theme]</button>
<script>
let popupRescheduleId = new URL(document.URL).searchParams.get("popupRescheduleId") || "qm3kwt3aTnVD7vmP9tiT2f"
document.write(`<button data-cal-namespace="popupReschedule" data-cal-link="reschedule/${popupRescheduleId}">Reschedule Event[Auto Theme]</button>`)
</script>
<button data-cal-namespace="popupPaidEvent" data-cal-link="pro/paid">Book Paid Event [Auto Theme]</button>
<button data-cal-namespace="popupHideEventTypeDetails" data-cal-link="free/30min">Book Free Event [Auto Theme][uiConfig.hideEventTypeDetails=true]</button>
<button data-cal-namespace="routingFormAuto" data-cal-link="forms/948ae412-d995-4865-875a-48302588de03">Book through Routing Form [Auto Theme]</button>

View File

@ -137,9 +137,9 @@ expect.extend({
};
}
}
let iframeReadyCheckInterval;
const iframeReadyEventDetail = await new Promise(async (resolve) => {
setInterval(async () => {
iframeReadyCheckInterval = setInterval(async () => {
const iframeReadyEventDetail = await getActionFiredDetails({
calNamespace,
actionType: "linkReady",
@ -150,6 +150,8 @@ expect.extend({
}, 500);
});
clearInterval(iframeReadyCheckInterval);
//At this point we know that window.initialBodyVisibility would be set as DOM would already have been ready(because linkReady event can only fire after that)
const {
visibility: visibilityBefore,

View File

@ -1,6 +1,6 @@
import { test as base, Page } from "@playwright/test";
interface Fixtures {
export interface Fixtures {
addEmbedListeners: (calNamespace: string) => Promise<void>;
getActionFiredDetails: (a: { calNamespace: string; actionType: string }) => Promise<any>;
}

View File

@ -3,8 +3,10 @@ import { Page, Frame, test, expect } from "@playwright/test";
import prisma from "@calcom/prisma";
export function todo(title: string) {
// eslint-disable-next-line @typescript-eslint/no-empty-function, playwright/no-skipped-test
test.skip(title, () => {});
}
export const deleteAllBookingsByEmail = async (email: string) =>
await prisma.booking.deleteMany({
where: {
@ -33,31 +35,41 @@ export const getBooking = async (bookingId: string) => {
export const getEmbedIframe = async ({ page, pathname }: { page: Page; pathname: string }) => {
// We can't seem to access page.frame till contentWindow is available. So wait for that.
await page.evaluate(() => {
const iframeReady = await page.evaluate(() => {
return new Promise((resolve) => {
const iframe = document.querySelector(".cal-embed") as HTMLIFrameElement;
if (!iframe) {
resolve(false);
return;
}
const interval = setInterval(() => {
const iframe = document.querySelector(".cal-embed") as HTMLIFrameElement | null;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (iframe.contentWindow && window.iframeReady) {
if (iframe && iframe.contentWindow && window.iframeReady) {
clearInterval(interval);
resolve(true);
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
console.log("Iframe Status:", !!iframe, !!iframe?.contentWindow, window.iframeReady);
}
}, 10);
}, 500);
// A hard timeout if iframe isn't ready in that time. Avoids infinite wait
setTimeout(() => {
clearInterval(interval);
resolve(false);
}, 5000);
});
});
const embedIframe = page.frame("cal-embed");
if (!embedIframe) {
if (!iframeReady) {
return null;
}
// We just verified that iframeReady is true here, so obviously embedIframe is not null
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const embedIframe = page.frame("cal-embed")!;
const u = new URL(embedIframe.url());
if (u.pathname === pathname + "/embed") {
return embedIframe;
}
console.log('Embed iframe url pathname match. Expected: "' + pathname + '/embed"', "Actual: " + u.pathname);
return null;
};
@ -91,7 +103,7 @@ export async function bookFirstEvent(username: string, frame: Frame, page: Page)
// It would also allow correct snapshot to be taken for current month.
await frame.waitForTimeout(1000);
expect(await page.screenshot()).toMatchSnapshot("availability-page-1.png");
const eventSlug = new URL(frame.url()).pathname;
await selectFirstAvailableTimeSlotNextMonth(frame, page);
await frame.waitForNavigation({
url(url) {
@ -104,10 +116,36 @@ export async function bookFirstEvent(username: string, frame: Frame, page: Page)
await frame.fill('[name="email"]', "embed-user@example.com");
await frame.press('[name="email"]', "Enter");
const response = await page.waitForResponse("**/api/book/event");
const responseObj = await response.json();
const bookingId = responseObj.uid;
const booking = (await response.json()) as { uid: string; eventSlug: string };
booking.eventSlug = eventSlug;
// Make sure we're navigated to the success page
await expect(frame.locator("[data-testid=success-page]")).toBeVisible();
expect(await page.screenshot()).toMatchSnapshot("success-page.png");
return bookingId;
//NOTE: frame.click('body') won't work here. Because the way it works, it clicks on the center of the body tag which is an element inside the popup view and that won't close the popup
await frame.evaluate(() => {
// Closes popup - if it is a popup. If not a popup, it will just do nothing
document.body.click();
});
return booking;
}
export async function rescheduleEvent(username, frame, page) {
await selectFirstAvailableTimeSlotNextMonth(frame, page);
await frame.waitForNavigation({
url(url) {
return url.pathname.includes(`/${username}/book`);
},
});
// --- fill form
await frame.press('[name="email"]', "Enter");
await frame.click("[data-testid=confirm-reschedule-button]");
const response = await page.waitForResponse("**/api/book/event");
const responseObj = await response.json();
const booking = responseObj.uid;
// Make sure we're navigated to the success page
await expect(frame.locator("[data-testid=success-page]")).toBeVisible();
return booking;
}

View File

@ -1,66 +1,131 @@
import { expect } from "@playwright/test";
import { expect, Page } from "@playwright/test";
import { test } from "../fixtures/fixtures";
import { todo, getEmbedIframe, bookFirstEvent, getBooking, deleteAllBookingsByEmail } from "../lib/testUtils";
import { Fixtures, test } from "../fixtures/fixtures";
import {
todo,
getEmbedIframe,
bookFirstEvent,
getBooking,
deleteAllBookingsByEmail,
rescheduleEvent,
} from "../lib/testUtils";
test("should open embed iframe on click - Configured with light theme", async ({
page,
async function bookFirstFreeUserEventThroughEmbed({
addEmbedListeners,
page,
getActionFiredDetails,
}) => {
await deleteAllBookingsByEmail("embed-user@example.com");
const calNamespace = "prerendertestLightTheme";
}: {
addEmbedListeners: Fixtures["addEmbedListeners"];
page: Page;
getActionFiredDetails: Fixtures["getActionFiredDetails"];
}) {
const embedButtonLocator = page.locator('[data-cal-link="free"]').first();
await page.goto("/");
// Obtain cal namespace from the element being clicked itself, so that addEmbedListeners always listen to correct namespace
const calNamespace = (await embedButtonLocator.getAttribute("data-cal-namespace")) || "";
await addEmbedListeners(calNamespace);
await page.goto("/?only=prerender-test");
let embedIframe = await getEmbedIframe({ page, pathname: "/free" });
expect(embedIframe).toBeFalsy();
// Goto / again so that initScript attached using addEmbedListeners can work now.
await page.goto("/");
await page.click('[data-cal-link="free?light&popup"]');
await embedButtonLocator.click();
embedIframe = await getEmbedIframe({ page, pathname: "/free" });
const embedIframe = await getEmbedIframe({ page, pathname: "/free" });
await expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, {
pathname: "/free",
});
expect(await page.screenshot()).toMatchSnapshot("event-types-list.png");
if (!embedIframe) {
throw new Error("Embed iframe not found");
}
const bookingId = await bookFirstEvent("free", embedIframe, page);
const booking = await getBooking(bookingId);
const booking = await bookFirstEvent("free", embedIframe, page);
return booking;
}
expect(booking.attendees.length).toBe(1);
await deleteAllBookingsByEmail("embed-user@example.com");
});
todo("Floating Button Test with Dark Theme");
todo("Floating Button Test with Light Theme");
todo("Add snapshot test for embed iframe");
test("should open Routing Forms embed on click", async ({
page,
addEmbedListeners,
getActionFiredDetails,
}) => {
await deleteAllBookingsByEmail("embed-user@example.com");
const calNamespace = "routingFormAuto";
await addEmbedListeners(calNamespace);
await page.goto("/?only=prerender-test");
let embedIframe = await getEmbedIframe({ page, pathname: "/forms/948ae412-d995-4865-875a-48302588de03" });
expect(embedIframe).toBeFalsy();
await page.click(
`[data-cal-namespace=${calNamespace}][data-cal-link="forms/948ae412-d995-4865-875a-48302588de03"]`
);
embedIframe = await getEmbedIframe({ page, pathname: "/forms/948ae412-d995-4865-875a-48302588de03" });
if (!embedIframe) {
throw new Error("Routing Form embed iframe not found");
}
await expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, {
pathname: "/forms/948ae412-d995-4865-875a-48302588de03",
test.describe("Popup Tests", () => {
test.afterEach(async () => {
await deleteAllBookingsByEmail("embed-user@example.com");
});
test("should open embed iframe on click - Configured with light theme", async ({
page,
addEmbedListeners,
getActionFiredDetails,
}) => {
await deleteAllBookingsByEmail("embed-user@example.com");
const calNamespace = "prerendertestLightTheme";
await addEmbedListeners(calNamespace);
await page.goto("/?only=prerender-test");
let embedIframe = await getEmbedIframe({ page, pathname: "/free" });
expect(embedIframe).toBeFalsy();
await page.click('[data-cal-link="free?light&popup"]');
embedIframe = await getEmbedIframe({ page, pathname: "/free" });
await expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, {
pathname: "/free",
});
expect(await page.screenshot()).toMatchSnapshot("event-types-list.png");
if (!embedIframe) {
throw new Error("Embed iframe not found");
}
const { uid: bookingId } = await bookFirstEvent("free", embedIframe, page);
const booking = await getBooking(bookingId);
expect(booking.attendees.length).toBe(1);
await deleteAllBookingsByEmail("embed-user@example.com");
});
test("should be able to reschedule", async ({ page, addEmbedListeners, getActionFiredDetails }) => {
const booking = await test.step("Create a booking", async () => {
return await bookFirstFreeUserEventThroughEmbed({
page,
addEmbedListeners,
getActionFiredDetails,
});
});
await test.step("Reschedule the booking", async () => {
await addEmbedListeners("popupReschedule");
await page.goto(`/?popupRescheduleId=${booking.uid}`);
await page.click('[data-cal-namespace="popupReschedule"]');
const embedIframe = await getEmbedIframe({ page, pathname: booking.eventSlug });
if (!embedIframe) {
throw new Error("Embed iframe not found");
}
await rescheduleEvent("free", embedIframe, page);
});
});
todo("Floating Button Test with Dark Theme");
todo("Floating Button Test with Light Theme");
todo("Add snapshot test for embed iframe");
test("should open Routing Forms embed on click", async ({
page,
addEmbedListeners,
getActionFiredDetails,
}) => {
await deleteAllBookingsByEmail("embed-user@example.com");
const calNamespace = "routingFormAuto";
await addEmbedListeners(calNamespace);
await page.goto("/?only=prerender-test");
let embedIframe = await getEmbedIframe({ page, pathname: "/forms/948ae412-d995-4865-875a-48302588de03" });
expect(embedIframe).toBeFalsy();
await page.click(
`[data-cal-namespace=${calNamespace}][data-cal-link="forms/948ae412-d995-4865-875a-48302588de03"]`
);
embedIframe = await getEmbedIframe({ page, pathname: "/forms/948ae412-d995-4865-875a-48302588de03" });
if (!embedIframe) {
throw new Error("Routing Form embed iframe not found");
}
await expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, {
pathname: "/forms/948ae412-d995-4865-875a-48302588de03",
});
await expect(embedIframe.locator("text=Seeded Form - Pro")).toBeVisible();
});
await expect(embedIframe.locator("text=Seeded Form - Pro")).toBeVisible();
});

View File

@ -174,14 +174,26 @@ const querySchema = z.object({
.default(AppCategories.calendar),
});
const AdminAppsList = ({ baseURL, className }: { baseURL: string; className?: string }) => (
<AppCategoryNavigation
baseURL={baseURL}
containerClassname="w-full xl:mx-5 xl:w-2/3 xl:pr-5"
className={className}>
<AdminAppsListContainer />
</AppCategoryNavigation>
);
const AdminAppsList = ({ baseURL, className }: { baseURL: string; className?: string }) => {
const router = useRouter();
return (
<form
id="wizard-step-2"
name="wizard-step-2"
onSubmit={(e) => {
e.preventDefault();
router.replace("/");
}}>
<AppCategoryNavigation
baseURL={baseURL}
fromAdmin
containerClassname="w-full xl:ml-5 max-h-97 overflow-scroll"
className={className}>
<AdminAppsListContainer />
</AppCategoryNavigation>
</form>
);
};
const AdminAppsListContainer = () => {
const { t } = useLocale();
@ -222,7 +234,7 @@ export default AdminAppsList;
const SkeletonLoader = () => {
return (
<SkeletonContainer>
<SkeletonContainer className="w-[30rem] pr-10">
<div className="mt-6 mb-8 space-y-6">
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />

View File

@ -7,6 +7,7 @@ import {
WebhookTriggerEvents,
} from "@prisma/client";
import async from "async";
import { isValidPhoneNumber } from "libphonenumber-js";
import { cloneDeep } from "lodash";
import type { NextApiRequest } from "next";
import short from "short-uuid";
@ -1065,7 +1066,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
await scheduleWorkflowReminders(
eventType.workflows,
reqBody.smsReminderNumber as string | null,
evt,
{ ...evt, ...{ metadata } },
evt.requiresConfirmation || false,
rescheduleUid ? true : false,
true
@ -1095,6 +1096,16 @@ function handleCustomInputs(
z.literal(true, {
errorMap: () => ({ message: `Missing ${etcInput.type} customInput: '${etcInput.label}'` }),
}).parse(input?.value);
} else if (etcInput.type === "PHONE") {
z.string({
errorMap: () => ({
message: `Missing ${etcInput.type} customInput: '${etcInput.label}'`,
}),
})
.refine((val) => isValidPhoneNumber(val), {
message: "Phone number is invalid",
})
.parse(input?.value);
} else {
// type: NUMBER are also passed as string
z.string({

View File

@ -12,8 +12,9 @@
"@hookform/resolvers": "^2.9.7",
"@sendgrid/client": "^7.7.0",
"@sendgrid/mail": "^7.6.2",
"libphonenumber-js": "^1.10.12",
"twilio": "^3.80.1",
"zod": "^3.19.1"
"zod": "^3.20.2"
},
"devDependencies": {
"@calcom/tsconfig": "*"

View File

@ -7,6 +7,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs";
import { defaultHandler } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import customTemplate, { VariablesType } from "../lib/reminders/templates/customTemplate";
import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate";
@ -125,6 +126,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
location: reminder.booking?.location || "",
additionalNotes: reminder.booking?.description,
customInputs: reminder.booking?.customInputs,
meetingUrl: bookingMetadataSchema.parse(reminder.booking?.metadata || {})?.videoCallUrl,
};
const emailSubject = await customTemplate(
reminder.workflowStep.emailSubject || "",
@ -147,16 +149,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const batchId = batchIdResponse[1].batch_id;
await sgMail.send({
to: sendTo,
from: senderEmail,
subject: emailContent.emailSubject,
text: emailContent.emailBody.text,
html: emailContent.emailBody.html,
batchId: batchId,
sendAt: dayjs(reminder.scheduledDate).unix(),
replyTo: reminder.booking?.user?.email || senderEmail,
});
if (reminder.workflowStep.action !== WorkflowActions.EMAIL_ADDRESS) {
await sgMail.send({
to: sendTo,
from: senderEmail,
subject: emailContent.emailSubject,
text: emailContent.emailBody.text,
html: emailContent.emailBody.html,
batchId: batchId,
sendAt: dayjs(reminder.scheduledDate).unix(),
replyTo: reminder.booking?.user?.email || senderEmail,
});
}
await prisma.workflowReminder.update({
where: {

View File

@ -5,6 +5,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs";
import { defaultHandler } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { getSenderId } from "../lib/alphanumericSenderIdSupport";
import * as twilio from "../lib/reminders/smsProviders/twilioProvider";
@ -98,6 +99,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
location: reminder.booking?.location || "",
additionalNotes: reminder.booking?.description,
customInputs: reminder.booking?.customInputs,
meetingUrl: bookingMetadataSchema.parse(reminder.booking?.metadata || {})?.videoCallUrl,
};
const customMessage = await customTemplate(
reminder.workflowStep.reminderBody || "",

View File

@ -16,6 +16,7 @@ const variables = [
"attendee_name",
"attendee_email",
"additional_notes",
"meeting_url",
];
export const AddVariablesDropdown = (props: IAddVariablesDropdown) => {

View File

@ -1,12 +1,11 @@
import React from "react";
import { Icon as FeatherIcon } from "react-feather";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SVGComponent } from "@calcom/types/SVGComponent";
import { Button, Icon } from "@calcom/ui";
type WorkflowExampleType = {
Icon: FeatherIcon;
Icon: SVGComponent;
text: string;
};
@ -40,7 +39,7 @@ export default function EmptyScreen({
isLoading,
showExampleWorkflows,
}: {
IconHeading: SVGComponent | FeatherIcon;
IconHeading: SVGComponent;
headline: string;
description: string | React.ReactElement;
buttonText?: string;

View File

@ -10,6 +10,7 @@ import sgMail from "@sendgrid/mail";
import dayjs from "@calcom/dayjs";
import prisma from "@calcom/prisma";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { BookingInfo, timeUnitLowerCase } from "./smsReminderManager";
import customTemplate, { VariablesType } from "./templates/customTemplate";
@ -39,6 +40,7 @@ export const scheduleEmailReminder = async (
workflowStepId: number,
template: WorkflowTemplates
) => {
if (action === WorkflowActions.EMAIL_ADDRESS) return;
const { startTime, endTime } = evt;
const uid = evt.uid as string;
const currentDate = dayjs();
@ -76,10 +78,6 @@ export const scheduleEmailReminder = async (
attendeeName = evt.organizer.name;
timeZone = evt.attendees[0].timeZone;
break;
case WorkflowActions.EMAIL_ADDRESS:
name = "";
attendeeName = evt.attendees[0].name;
timeZone = evt.organizer.timeZone;
}
let emailContent = {
@ -106,6 +104,7 @@ export const scheduleEmailReminder = async (
location: evt.location,
additionalNotes: evt.additionalNotes,
customInputs: evt.customInputs,
meetingUrl: bookingMetadataSchema.parse(evt.metadata || {})?.videoCallUrl,
};
const emailSubjectTemplate = await customTemplate(

View File

@ -19,7 +19,7 @@ export const scheduleWorkflowReminders = async (
};
})[],
smsReminderNumber: string | null,
evt: CalendarEvent,
evt: CalendarEvent & { metadata?: { videoCallUrl: string } },
needsConfirmation: boolean,
isRescheduleEvent: boolean,
isFirstRecurringEvent: boolean
@ -58,8 +58,7 @@ export const scheduleWorkflowReminders = async (
);
} else if (
step.action === WorkflowActions.EMAIL_ATTENDEE ||
step.action === WorkflowActions.EMAIL_HOST ||
step.action === WorkflowActions.EMAIL_ADDRESS
step.action === WorkflowActions.EMAIL_HOST
) {
let sendTo = "";
@ -70,9 +69,8 @@ export const scheduleWorkflowReminders = async (
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = evt.attendees[0].email;
break;
case WorkflowActions.EMAIL_ADDRESS:
sendTo = step.sendTo || "";
}
scheduleEmailReminder(
evt,
workflow.trigger,
@ -129,8 +127,7 @@ export const sendCancelledReminders = async (
);
} else if (
step.action === WorkflowActions.EMAIL_ATTENDEE ||
step.action === WorkflowActions.EMAIL_HOST ||
step.action === WorkflowActions.EMAIL_ADDRESS
step.action === WorkflowActions.EMAIL_HOST
) {
let sendTo = "";
@ -141,8 +138,6 @@ export const sendCancelledReminders = async (
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = evt.attendees[0].email;
break;
case WorkflowActions.EMAIL_ADDRESS:
sendTo = step.sendTo || "";
}
scheduleEmailReminder(
evt,

View File

@ -9,6 +9,7 @@ import {
import dayjs from "@calcom/dayjs";
import prisma from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { getSenderId } from "../alphanumericSenderIdSupport";
import * as twilio from "./smsProviders/twilioProvider";
@ -36,6 +37,7 @@ export type BookingInfo = {
location?: string | null;
additionalNotes?: string | null;
customInputs?: Prisma.JsonValue;
metadata?: Prisma.JsonValue;
};
export const scheduleSMSReminder = async (
@ -101,6 +103,7 @@ export const scheduleSMSReminder = async (
location: evt.location,
additionalNotes: evt.additionalNotes,
customInputs: evt.customInputs,
meetingUrl: bookingMetadataSchema.parse(evt.metadata || {})?.videoCallUrl,
};
const customMessage = await customTemplate(message, variables, evt.organizer.language.locale);
message = customMessage.text;

View File

@ -13,6 +13,7 @@ export type VariablesType = {
location?: string | null;
additionalNotes?: string | null;
customInputs?: Prisma.JsonValue;
meetingUrl?: string;
};
const customTemplate = async (text: string, variables: VariablesType, locale: string) => {
@ -33,7 +34,8 @@ const customTemplate = async (text: string, variables: VariablesType, locale: st
.replaceAll("{EVENT_TIME}", timeWithTimeZone)
.replaceAll("{LOCATION}", locationString)
.replaceAll("{ADDITIONAL_NOTES}", variables.additionalNotes || "")
.replaceAll("{ATTENDEE_EMAIL}", variables.attendeeEmail || "");
.replaceAll("{ATTENDEE_EMAIL}", variables.attendeeEmail || "")
.replaceAll("{MEETING_URL}", variables.meetingUrl || "");
const customInputvariables = dynamicText.match(/\{(.+?)}/g)?.map((variable) => {
return variable.replace("{", "").replace("}", "");

View File

@ -2,6 +2,7 @@ import { useState, useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
import dayjs, { Dayjs } from "@calcom/dayjs";
import { classNames } from "@calcom/lib";
import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { WorkingHours } from "@calcom/types/schedule";
@ -113,8 +114,8 @@ const DateOverrideForm = ({
);
onClose();
}}
className="flex space-x-4">
<div className="w-1/2 border-r pr-6">
className="space-y-4 sm:flex sm:space-x-4">
<div className={classNames(date && "w-full sm:border-r sm:pr-6")}>
<DialogHeader title={t("date_overrides_dialog_title")} />
<DatePicker
includedDates={includedDates}
@ -130,7 +131,7 @@ const DateOverrideForm = ({
/>
</div>
{date && (
<div className="relative flex w-1/2 flex-col pl-2">
<div className="relative flex w-full flex-col sm:pl-2">
<div className="mb-4 flex-grow space-y-4">
<p className="text-medium text-sm">{t("date_overrides_dialog_which_hours")}</p>
<div>

View File

@ -51,7 +51,7 @@ const ScheduleDay = <TFieldValues extends FieldValues>({
const watchDayRange = watch(name);
return (
<div className="mb-1 flex w-full flex-col py-1 sm:flex-row">
<div className="mb-1 flex w-full flex-col px-5 py-1 sm:flex-row sm:px-0">
{/* Label & switch container */}
<div className="flex h-11 items-center justify-between sm:w-32">
<div>
@ -83,7 +83,6 @@ const ScheduleDay = <TFieldValues extends FieldValues>({
<SkeletonText className="mt-2.5 ml-1 h-6 w-48" />
)}
</>
<div className="my-2 h-[1px] w-full bg-gray-200 sm:hidden" />
</div>
);
};
@ -140,7 +139,7 @@ const Schedule = <
const { i18n } = useLocale();
return (
<>
<div className="divide-y sm:divide-none">
{/* First iterate for each day */}
{weekdayNames(i18n.language, weekStart, "long").map((weekday, num) => {
const weekdayIndex = (num + weekStart) % 7;
@ -155,7 +154,7 @@ const Schedule = <
/>
);
})}
</>
</div>
);
};

View File

@ -77,7 +77,7 @@ export const getAppsStatus = (calEvent: CalendarEvent) => {
if (!calEvent.appsStatus) {
return "";
}
return `\n${calEvent.attendees[0].language.translate("apps_status")}
return `\n${calEvent.organizer.language.translate("apps_status")}
${calEvent.appsStatus.map((app) => {
return `\n- ${app.appName} ${
app.success >= 1 ? `${app.success > 1 ? `(x${app.success})` : ""}` : ""
@ -94,7 +94,7 @@ export const getDescription = (calEvent: CalendarEvent) => {
if (!calEvent.description) {
return "";
}
return `\n${calEvent.attendees[0].language.translate("description")}
return `\n${calEvent.organizer.language.translate("description")}
${calEvent.description}
`;
};

View File

@ -13,9 +13,9 @@
"dependencies": {
"@calcom/config": "*",
"@calcom/dayjs": "*",
"@prisma/client": "^4.2.1",
"@prisma/client": "^4.7.1",
"@sendgrid/client": "^7.7.0",
"@vercel/og": "^0.0.19",
"@vercel/og": "^0.0.21",
"bcryptjs": "^2.4.3",
"ical.js": "^1.4.0",
"ics": "^2.37.0",

View File

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "EventTypeCustomInputType" ADD VALUE 'phone';

View File

@ -24,10 +24,10 @@
},
"dependencies": {
"@calcom/lib": "*",
"@prisma/client": "^4.2.1",
"prisma": "^4.2.1",
"@prisma/client": "^4.7.1",
"prisma": "^4.7.1",
"ts-node": "^10.9.1",
"zod": "^3.19.1",
"zod": "^3.20.2",
"zod-prisma": "^0.5.4"
},
"main": "index.ts",

View File

@ -344,6 +344,7 @@ enum EventTypeCustomInputType {
NUMBER @map("number")
BOOL @map("bool")
RADIO @map("radio")
PHONE @map("phone")
}
model EventTypeCustomInput {

View File

@ -149,5 +149,11 @@
"categories": ["calendar"],
"slug": "vimcal",
"type": "vimcal_other"
},
{
"dirName": "amie",
"categories": ["calendar"],
"slug": "amie",
"type": "amie_other"
}
]

View File

@ -12,6 +12,6 @@
"@trpc/react-query": "^10.0.0-rc.6",
"@trpc/server": "^10.0.0-rc.6",
"superjson": "1.9.1",
"zod": "^3.19.1"
"zod": "^3.20.2"
}
}

View File

@ -10,7 +10,7 @@ import {
} from "@prisma/client";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
// import dayjs from "@calcom/dayjs";
import {
WORKFLOW_TEMPLATES,
WORKFLOW_TRIGGER_EVENTS,
@ -23,7 +23,7 @@ import {
scheduleEmailReminder,
} from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
import {
BookingInfo,
// BookingInfo,
deleteScheduledSMSReminder,
scheduleSMSReminder,
} from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
@ -32,7 +32,7 @@ import {
sendVerificationCode,
} from "@calcom/features/ee/workflows/lib/reminders/verifyPhoneNumber";
import { SENDER_ID } from "@calcom/lib/constants";
import { getErrorFromUnknown } from "@calcom/lib/errors";
// import { getErrorFromUnknown } from "@calcom/lib/errors";
import { getTranslation } from "@calcom/lib/server/i18n";
import { TRPCError } from "@trpc/server";
@ -443,8 +443,8 @@ export const workflowsRouter = router({
};
if (
step.action === WorkflowActions.EMAIL_HOST ||
step.action === WorkflowActions.EMAIL_ATTENDEE ||
step.action === WorkflowActions.EMAIL_ADDRESS
step.action === WorkflowActions.EMAIL_ATTENDEE /*||
step.action === WorkflowActions.EMAIL_ADDRESS*/
) {
let sendTo = "";
@ -455,8 +455,8 @@ export const workflowsRouter = router({
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = bookingInfo.attendees[0].email;
break;
case WorkflowActions.EMAIL_ADDRESS:
sendTo = step.sendTo || "";
/*case WorkflowActions.EMAIL_ADDRESS:
sendTo = step.sendTo || "";*/
}
await scheduleEmailReminder(
@ -550,8 +550,8 @@ export const workflowsRouter = router({
data: {
action: newStep.action,
sendTo:
newStep.action === WorkflowActions.SMS_NUMBER ||
newStep.action === WorkflowActions.EMAIL_ADDRESS
newStep.action === WorkflowActions.SMS_NUMBER /*||
newStep.action === WorkflowActions.EMAIL_ADDRESS*/
? newStep.sendTo
: null,
stepNumber: newStep.stepNumber,
@ -630,8 +630,8 @@ export const workflowsRouter = router({
};
if (
newStep.action === WorkflowActions.EMAIL_HOST ||
newStep.action === WorkflowActions.EMAIL_ATTENDEE ||
newStep.action === WorkflowActions.EMAIL_ADDRESS
newStep.action === WorkflowActions.EMAIL_ATTENDEE /*||
newStep.action === WorkflowActions.EMAIL_ADDRESS*/
) {
let sendTo = "";
@ -642,8 +642,8 @@ export const workflowsRouter = router({
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = bookingInfo.attendees[0].email;
break;
case WorkflowActions.EMAIL_ADDRESS:
sendTo = newStep.sendTo || "";
/*case WorkflowActions.EMAIL_ADDRESS:
sendTo = newStep.sendTo || "";*/
}
await scheduleEmailReminder(
@ -747,8 +747,8 @@ export const workflowsRouter = router({
if (
step.action === WorkflowActions.EMAIL_ATTENDEE ||
step.action === WorkflowActions.EMAIL_HOST ||
step.action === WorkflowActions.EMAIL_ADDRESS
step.action === WorkflowActions.EMAIL_HOST /*||
step.action === WorkflowActions.EMAIL_ADDRESS*/
) {
let sendTo = "";
@ -759,8 +759,8 @@ export const workflowsRouter = router({
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = bookingInfo.attendees[0].email;
break;
case WorkflowActions.EMAIL_ADDRESS:
sendTo = step.sendTo || "";
/*case WorkflowActions.EMAIL_ADDRESS:
sendTo = step.sendTo || "";*/
}
await scheduleEmailReminder(
@ -850,148 +850,150 @@ export const workflowsRouter = router({
reminderBody: z.string(),
})
)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.mutation(async ({ ctx, input }) => {
const { user } = ctx;
const { step, emailSubject, reminderBody } = input;
const { action, template, sendTo, sender } = step;
throw new TRPCError({ code: "FORBIDDEN", message: "Test action temporarily disabled" });
// const { user } = ctx;
// const { step, emailSubject, reminderBody } = input;
// const { action, template, sendTo, sender } = step;
const senderID = sender || SENDER_ID;
// const senderID = sender || SENDER_ID;
if (action === WorkflowActions.SMS_NUMBER) {
if (!sendTo) throw new TRPCError({ code: "BAD_REQUEST", message: "Missing sendTo" });
const verifiedNumbers = await ctx.prisma.verifiedNumber.findFirst({
where: {
userId: ctx.user.id,
phoneNumber: sendTo,
},
});
if (!verifiedNumbers)
throw new TRPCError({ code: "UNAUTHORIZED", message: "Phone number is not verified" });
}
// if (action === WorkflowActions.SMS_NUMBER) {
// if (!sendTo) throw new TRPCError({ code: "BAD_REQUEST", message: "Missing sendTo" });
// const verifiedNumbers = await ctx.prisma.verifiedNumber.findFirst({
// where: {
// userId: ctx.user.id,
// phoneNumber: sendTo,
// },
// });
// if (!verifiedNumbers)
// throw new TRPCError({ code: "UNAUTHORIZED", message: "Phone number is not verified" });
// }
try {
const userWorkflow = await ctx.prisma.workflow.findUnique({
where: {
id: step.workflowId,
},
select: {
userId: true,
steps: true,
},
});
// try {
// const userWorkflow = await ctx.prisma.workflow.findUnique({
// where: {
// id: step.workflowId,
// },
// select: {
// userId: true,
// steps: true,
// },
// });
if (!userWorkflow || userWorkflow.userId !== user.id) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
// if (!userWorkflow || userWorkflow.userId !== user.id) {
// throw new TRPCError({ code: "UNAUTHORIZED" });
// }
if (isSMSAction(step.action)) {
const hasTeamPlan = (await ctx.prisma.membership.count({ where: { userId: user.id } })) > 0;
if (!hasTeamPlan) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Team plan needed" });
}
}
// if (isSMSAction(step.action) /*|| step.action === WorkflowActions.EMAIL_ADDRESS*/) {
// const hasTeamPlan = (await ctx.prisma.membership.count({ where: { userId: user.id } })) > 0;
// if (!hasTeamPlan) {
// throw new TRPCError({ code: "UNAUTHORIZED", message: "Team plan needed" });
// }
// }
const booking = await ctx.prisma.booking.findFirst({
orderBy: {
createdAt: "desc",
},
where: {
userId: ctx.user.id,
},
include: {
attendees: true,
user: true,
},
});
// const booking = await ctx.prisma.booking.findFirst({
// orderBy: {
// createdAt: "desc",
// },
// where: {
// userId: ctx.user.id,
// },
// include: {
// attendees: true,
// user: true,
// },
// });
let evt: BookingInfo;
if (booking) {
evt = {
uid: booking?.uid,
attendees:
booking?.attendees.map((attendee) => {
return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone };
}) || [],
organizer: {
language: {
locale: booking?.user?.locale || "",
},
name: booking?.user?.name || "",
email: booking?.user?.email || "",
timeZone: booking?.user?.timeZone || "",
},
startTime: booking?.startTime.toISOString() || "",
endTime: booking?.endTime.toISOString() || "",
title: booking?.title || "",
location: booking?.location || null,
additionalNotes: booking?.description || null,
customInputs: booking?.customInputs,
};
} else {
//if no booking exists create an example booking
evt = {
attendees: [{ name: "John Doe", email: "john.doe@example.com", timeZone: "Europe/London" }],
organizer: {
language: {
locale: ctx.user.locale,
},
name: ctx.user.name || "",
email: ctx.user.email,
timeZone: ctx.user.timeZone,
},
startTime: dayjs().add(10, "hour").toISOString(),
endTime: dayjs().add(11, "hour").toISOString(),
title: "Example Booking",
location: "Office",
additionalNotes: "These are additional notes",
};
}
// let evt: BookingInfo;
// if (booking) {
// evt = {
// uid: booking?.uid,
// attendees:
// booking?.attendees.map((attendee) => {
// return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone };
// }) || [],
// organizer: {
// language: {
// locale: booking?.user?.locale || "",
// },
// name: booking?.user?.name || "",
// email: booking?.user?.email || "",
// timeZone: booking?.user?.timeZone || "",
// },
// startTime: booking?.startTime.toISOString() || "",
// endTime: booking?.endTime.toISOString() || "",
// title: booking?.title || "",
// location: booking?.location || null,
// additionalNotes: booking?.description || null,
// customInputs: booking?.customInputs,
// };
// } else {
// //if no booking exists create an example booking
// evt = {
// attendees: [{ name: "John Doe", email: "john.doe@example.com", timeZone: "Europe/London" }],
// organizer: {
// language: {
// locale: ctx.user.locale,
// },
// name: ctx.user.name || "",
// email: ctx.user.email,
// timeZone: ctx.user.timeZone,
// },
// startTime: dayjs().add(10, "hour").toISOString(),
// endTime: dayjs().add(11, "hour").toISOString(),
// title: "Example Booking",
// location: "Office",
// additionalNotes: "These are additional notes",
// };
// }
if (
action === WorkflowActions.EMAIL_ATTENDEE ||
action === WorkflowActions.EMAIL_HOST ||
action === WorkflowActions.EMAIL_ADDRESS
) {
scheduleEmailReminder(
evt,
WorkflowTriggerEvents.NEW_EVENT,
action,
{ time: null, timeUnit: null },
ctx.user.email,
emailSubject,
reminderBody,
0,
template
);
return { message: "Notification sent" };
} else if (action === WorkflowActions.SMS_NUMBER && sendTo) {
scheduleSMSReminder(
evt,
sendTo,
WorkflowTriggerEvents.NEW_EVENT,
action,
{ time: null, timeUnit: null },
reminderBody,
0,
template,
senderID,
ctx.user.id
);
return { message: "Notification sent" };
}
return {
ok: false,
status: 500,
message: "Notification could not be sent",
};
} catch (_err) {
const error = getErrorFromUnknown(_err);
return {
ok: false,
status: 500,
message: error.message,
};
}
// if (
// action === WorkflowActions.EMAIL_ATTENDEE ||
// action === WorkflowActions.EMAIL_HOST /*||
// action === WorkflowActions.EMAIL_ADDRESS*/
// ) {
// scheduleEmailReminder(
// evt,
// WorkflowTriggerEvents.NEW_EVENT,
// action,
// { time: null, timeUnit: null },
// ctx.user.email,
// emailSubject,
// reminderBody,
// 0,
// template
// );
// return { message: "Notification sent" };
// } else if (action === WorkflowActions.SMS_NUMBER && sendTo) {
// scheduleSMSReminder(
// evt,
// sendTo,
// WorkflowTriggerEvents.NEW_EVENT,
// action,
// { time: null, timeUnit: null },
// reminderBody,
// 0,
// template,
// senderID,
// ctx.user.id
// );
// return { message: "Notification sent" };
// }
// return {
// ok: false,
// status: 500,
// message: "Notification could not be sent",
// };
// } catch (_err) {
// const error = getErrorFromUnknown(_err);
// return {
// ok: false,
// status: 500,
// message: error.message,
// };
// }
}),
activateEventType: authedProcedure
.input(

View File

@ -75,7 +75,7 @@ export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps
: props.size == "lg"
? "p-6 sm:max-w-[70rem]"
: "p-6 sm:max-w-[35rem]",
"max-h-[560px] overflow-visible overscroll-auto md:h-auto md:max-h-[inherit]",
"overflow-visible overscroll-auto md:h-auto md:max-h-[inherit]",
`${props.className || ""}`
)}
ref={forwardedRef}>

View File

@ -1,9 +1,9 @@
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as Tooltip from "@radix-ui/react-tooltip";
import { Check } from "react-feather";
import classNames from "@calcom/lib/classNames";
import { defaultAvatarSrc } from "@calcom/lib/defaultAvatarImage";
import { Icon } from "@calcom/ui";
import { Maybe } from "@trpc/server";
@ -54,7 +54,7 @@ export function Avatar(props: AvatarProps) {
size === "lg" ? "h-5 w-5" : "h-2 w-2"
)}>
<div className="flex h-full items-center justify-center p-[2px]">
{size === "lg" && <Check className="" />}
{size === "lg" && <Icon.FiCheck />}
</div>
</div>
)}

View File

@ -1,7 +1,7 @@
import { Icon } from "react-feather";
import { GoPrimitiveDot } from "react-icons/go";
import classNames from "@calcom/lib/classNames";
import { SVGComponent } from "@calcom/types/SVGComponent";
const badgeClassNameByVariant = {
default: "bg-orange-100 text-orange-800",
@ -23,7 +23,7 @@ const classNameBySize = {
export type BadgeProps = {
variant: keyof typeof badgeClassNameByVariant;
size?: keyof typeof classNameBySize;
StartIcon?: Icon;
StartIcon?: SVGComponent;
bold?: boolean;
withDot?: boolean;
rounded?: boolean;

View File

@ -1,10 +1,10 @@
import { cva, VariantProps } from "class-variance-authority";
import Link, { LinkProps } from "next/link";
import React, { forwardRef } from "react";
import { Icon } from "react-feather";
import classNames from "@calcom/lib/classNames";
import { applyStyleToMultipleVariants } from "@calcom/lib/cva";
import { SVGComponent } from "@calcom/types/SVGComponent";
import { Tooltip } from "@calcom/ui";
type InferredVariantProps = VariantProps<typeof buttonClasses>;
@ -13,9 +13,9 @@ export type ButtonBaseProps = {
/** Action that happens when the button is clicked */
onClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
/**Left aligned icon*/
StartIcon?: Icon | React.ElementType;
StartIcon?: SVGComponent | React.ElementType;
/**Right aligned icon */
EndIcon?: Icon;
EndIcon?: SVGComponent;
shallow?: boolean;
/**Tool tip used when icon size is set to small */
tooltip?: string;

View File

@ -16,3 +16,4 @@ export {
InputFieldWithSelect,
} from "./inputs/Input";
export { Label } from "./inputs/Label";
export { Select, SelectField, SelectWithValidation, getReactSelectProps } from "./select";

View File

@ -1,4 +1,3 @@
import { X, Circle, Check } from "react-feather";
import { FieldValues, useFormContext } from "react-hook-form";
// TODO: Refactor import once V1 migration has happened
@ -50,12 +49,12 @@ export function HintsOrErrors<T extends FieldValues = FieldValues>(props: {
className={error !== undefined ? (submitted ? "text-red-700" : "") : "text-green-600"}>
{error !== undefined ? (
submitted ? (
<X size="12" strokeWidth="3" className="mr-2 -ml-1 inline-block" />
<Icon.FiX size="12" strokeWidth="3" className="mr-2 -ml-1 inline-block" />
) : (
<Circle fill="currentColor" size="5" className="mr-2 inline-block" />
<Icon.FiCircle fill="currentColor" size="5" className="mr-2 inline-block" />
)
) : (
<Check size="12" strokeWidth="3" className="mr-2 -ml-1 inline-block" />
<Icon.FiCheck size="12" strokeWidth="3" className="mr-2 -ml-1 inline-block" />
)}
{t(`${fieldName}_hint_${key}`)}
</li>

View File

@ -1,5 +1,4 @@
import React, { forwardRef, ReactElement, ReactNode, Ref, useCallback, useId, useState } from "react";
import { Eye, EyeOff } from "react-feather";
import { FieldValues, FormProvider, SubmitHandler, useFormContext, UseFormReturn } from "react-hook-form";
import classNames from "@calcom/lib/classNames";
@ -211,9 +210,9 @@ export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(funct
type="button"
onClick={() => toggleIsPasswordVisible()}>
{isPasswordVisible ? (
<EyeOff className="h-4 stroke-[2.5px]" />
<Icon.FiEyeOff className="h-4 stroke-[2.5px]" />
) : (
<Eye className="h-4 stroke-[2.5px]" />
<Icon.FiEye className="h-4 stroke-[2.5px]" />
)}
<span className="sr-only">{textLabel}</span>
</button>

View File

@ -12,7 +12,7 @@ import ReactSelect, {
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Label } from "../../../../components/form/inputs/Label";
import { Label } from "../inputs/Label";
import {
ControlComponent,
InputComponent,
@ -58,7 +58,7 @@ export const getReactSelectProps = <
},
});
const Select = <
export const Select = <
Option,
IsMulti extends boolean = false,
Group extends GroupBase<Option> = GroupBase<Option>
@ -93,6 +93,20 @@ const Select = <
);
};
type IconLeadingProps = {
icon: React.ReactNode;
children?: React.ReactNode;
} & React.ComponentProps<typeof reactSelectComponents.Control>;
export const IconLeading = ({ icon, children, ...props }: IconLeadingProps) => {
return (
<reactSelectComponents.Control {...props}>
{icon}
{children}
</reactSelectComponents.Control>
);
};
export const SelectField = function SelectField<
Option,
IsMulti extends boolean = false,
@ -194,5 +208,3 @@ export function SelectWithValidation<
</div>
);
}
export default Select;

View File

@ -13,7 +13,7 @@ import {
import { classNames } from "@calcom/lib";
import { Icon } from "../../../..";
import { Icon } from "../../../Icon";
export const InputComponent = <
Option,

View File

@ -0,0 +1 @@
export { SelectWithValidation, SelectField, getReactSelectProps, Select } from "./Select";

View File

@ -0,0 +1,115 @@
import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs';
import { Examples, Example, Note, Title,CustomArgsTable, VariantRow,VariantsTable} from '@calcom/storybook/components'
import { Icon } from "@calcom/ui/Icon";
import {SelectField} from "./Select"
<Meta title="UI/Form/Select Field" component={SelectField} />
<Title title="Select" suffix="Brief" subtitle="Version 2.0 — Last Update: 22 Aug 2022"/>
## Definition
Dropdown fields allow users to input existing options that is preset by the deisgner/ developer. It can be just one choice per field, or they might be multiple choices depends on the circumstances.
## Structure
<CustomArgsTable of={SelectField} />
export const options = [
{ value: 0, label: "Option One" },
{ value: 1, label: "Option Two" },
];
## Examples
<Examples title=" Single Selected / Unselected" footnote={
<ul>
<li>The difference between the types are when they are filled. </li>
</ul>
}>
<Example title="Single Select [Unselected]">
<SelectField
label={"Single Select"}
options={options}
/>
</Example>
<Example title="Single Select [Selected]">
<SelectField
label={"Single Select"}
options={options}
defaultValue={options[0]}
/>
</Example>
<Example title="Multi Select [Unselected]">
<SelectField
label={"Multi Select"}
options={options}
isMulti={true}
/>
</Example>
<Example title="Multi Select [Selected]">
<SelectField
label={"Multi Select"}
options={options}
isMulti={true}
defaultValue={options[0]}
/>
</Example>
</Examples>
<Examples title="Variants">
<Example title="Default">
<SelectField
label={"Default Select"}
options={options}/>
</Example>
<Example title="Icon Left">
WIP
{/* <SelectField options={options} components={{ Control }}/> */}
</Example>
</Examples>
## Variant Caviats (WIP) - To be updated
Using Icons is a bit of a strange one cause you can't simpily pass in an icon as a prop. You have to pass in a component. To the select field.
```js
// Bad: Inline declaration will cause remounting issues
const BadSelect = props => (
<Select {...props} components={{
Control: ({ children, ...rest }) => (
<components.Control {...rest}>
👎 {children}
</components.Control>
)}}
/>
)
// Good: Custom component declared outside of the Select scope
const Control = <IconLeading icon={Icon.FiPlus}/>
const GoodSelect = props => <Select {...props} components={{ Control }} />
```
<Examples title="States ">
<Example title="Default">
<SelectField options={options} label={"Default Select"}/>
</Example>
{/* <Example title="Hover">
<SelectField options={options} className="sb-pseudo--hover"/>
</Example>
<Example title="Focus">
<SelectField options={options} className="sb-pseudo--focus"/>
</Example> */}
</Examples>
## Select Story
<Canvas>
<Story name="Default">
<SelectField options={options} label={"Default Select"}/>
</Story>
</Canvas>

View File

@ -23,6 +23,9 @@ export {
TextAreaField,
TextField,
InputFieldWithSelect,
Select,
SelectField,
SelectWithValidation,
} from "./form";
export { TopBanner } from "./top-banner";
export type { TopBannerProps } from "./top-banner";

View File

@ -7,7 +7,7 @@ import BaseSelect, {
Props as SelectProps,
} from "react-timezone-select";
import { InputComponent } from "../v2/core/form/select/components";
import { InputComponent } from "../components/form/select/components";
function TimezoneSelect({ className, ...props }: SelectProps) {
// @TODO: remove borderRadius and haveRoundedClassName logic from theme so we use only new style

View File

@ -25,6 +25,9 @@ export {
TextField,
TopBanner,
AnimatedPopover,
Select,
SelectField,
SelectWithValidation,
} from "./components";
export type { AvatarProps, BadgeProps, ButtonBaseProps, ButtonProps, TopBannerProps } from "./components";
export { default as CheckboxField } from "./components/form/checkbox/Checkbox";
@ -44,15 +47,11 @@ export {
CustomInputItem,
EmptyScreen,
HorizontalTabs,
Select,
SelectField,
SelectWithValidation,
SettingsToggle,
showToast,
SkeletonAvatar,
SkeletonButton,
SkeletonContainer,
OptionComponentWithIcon,
SkeletonText,
Swatch,
Switch,
@ -60,7 +59,7 @@ export {
TipBanner,
} from "./v2";
export type { AlertProps } from "./v2";
export { getReactSelectProps, Segment, SegmentOption } from "./v2/core";
export { Segment, SegmentOption } from "./v2/core";
export { default as AllApps } from "./v2/core/apps/AllApps";
export { default as AppCard } from "./v2/core/apps/AppCard";
export { default as AppStoreCategories } from "./v2/core/apps/Categories";

View File

@ -1,10 +1,10 @@
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { useRouter } from "next/router";
import React, { ReactNode, useState } from "react";
import { Icon } from "react-feather";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SVGComponent } from "@calcom/types/SVGComponent";
import { Button, ButtonProps } from "../../components/button";
@ -58,7 +58,7 @@ type DialogContentProps = React.ComponentProps<typeof DialogPrimitive["Content"]
description?: string | JSX.Element | undefined;
closeText?: string;
actionDisabled?: boolean;
Icon?: Icon;
Icon?: SVGComponent;
};
export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
@ -78,7 +78,7 @@ export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps
: props.size == "md"
? "p-8 sm:max-w-[48rem]"
: "p-8 sm:max-w-[35rem]",
"max-h-[560px] overflow-visible overscroll-auto md:h-auto md:max-h-[inherit]",
"overflow-y-auto overscroll-auto md:h-auto md:max-h-[inherit]",
`${props.className || ""}`
)}
ref={forwardedRef}>

View File

@ -2,9 +2,9 @@ import { CheckCircleIcon } from "@heroicons/react/outline";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import Link from "next/link";
import { ComponentProps, forwardRef } from "react";
import { Icon } from "react-feather";
import { classNames } from "@calcom/lib";
import { SVGComponent } from "@calcom/types/SVGComponent";
export const Dropdown = DropdownMenuPrimitive.Root;
@ -98,8 +98,8 @@ DropdownMenuRadioItem.displayName = "DropdownMenuRadioItem";
type DropdownItemProps = {
children: React.ReactNode;
color?: "destructive";
StartIcon?: Icon;
EndIcon?: Icon;
StartIcon?: SVGComponent;
EndIcon?: SVGComponent;
href?: string;
disabled?: boolean;
} & ButtonOrLinkProps;

View File

@ -1,5 +1,4 @@
import React, { ReactNode } from "react";
import { Icon } from "react-feather";
import { IconType } from "react-icons";
import { SVGComponent } from "@calcom/types/SVGComponent";
@ -14,7 +13,7 @@ export default function EmptyScreen({
buttonOnClick,
buttonRaw,
}: {
Icon: SVGComponent | Icon | IconType;
Icon: SVGComponent | IconType;
headline: string;
description: string | React.ReactElement;
buttonText?: string;

View File

@ -6,7 +6,7 @@ import BaseSelect, {
Props as SelectProps,
} from "react-timezone-select";
import { getReactSelectProps } from "../..";
import { getReactSelectProps } from "../../components/form";
function TimezoneSelect({ className, components, ...props }: SelectProps) {
const reactSelectProps = useMemo(() => {

View File

@ -42,7 +42,7 @@ function WizardForm<T extends DefaultStep>(props: {
<p className="text-sm text-gray-500">{currentStep.description}</p>
</div>
<div className="print:p-none px-4 py-5 sm:p-6">{currentStep.content}</div>
<div className="print:p-none max-w-3xl px-4 py-5 sm:p-6">{currentStep.content}</div>
{!props.disableNavigation && (
<>
{currentStep.enabled !== false && (

View File

@ -2,12 +2,13 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { Credential } from "@prisma/client";
import { useRouter } from "next/router";
import { UIEvent, useEffect, useRef, useState } from "react";
import { ChevronLeft, ChevronRight } from "react-feather";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { App } from "@calcom/types/App";
import { Icon } from "@calcom/ui";
import EmptyScreen from "../EmptyScreen";
import AppCard from "./AppCard";
export function useShouldShowArrows() {
@ -73,7 +74,7 @@ function CategoryTab({ selectedCategory, categories, searchText }: CategoryTabPr
{leftVisible && (
<button onClick={handleLeft} className="absolute top-9 flex md:left-1/2 md:-top-1">
<div className="flex h-12 w-5 items-center justify-end bg-white">
<ChevronLeft className="h-4 w-4 text-gray-500" />
<Icon.FiChevronLeft className="h-4 w-4 text-gray-500" />
</div>
<div className="flex h-12 w-5 bg-gradient-to-l from-transparent to-white" />
</button>
@ -116,7 +117,7 @@ function CategoryTab({ selectedCategory, categories, searchText }: CategoryTabPr
<button onClick={handleRight} className="absolute top-9 right-0 flex md:-top-1">
<div className="flex h-12 w-5 bg-gradient-to-r from-transparent to-white" />
<div className="flex h-12 w-5 items-center justify-end bg-white">
<ChevronRight className="h-4 w-4 text-gray-500" />
<Icon.FiChevronRight className="h-4 w-4 text-gray-500" />
</div>
</button>
)}
@ -137,7 +138,7 @@ export default function AllApps({ apps, searchText }: AllAppsPropsType) {
});
if (searchText) {
enableAnimation(false);
enableAnimation && enableAnimation(false);
}
useEffect(() => {
@ -161,15 +162,21 @@ export default function AllApps({ apps, searchText }: AllAppsPropsType) {
return (
<div className="mb-16">
<CategoryTab selectedCategory={selectedCategory} searchText={searchText} categories={categories} />
<div
className="grid gap-3 lg:grid-cols-4 [@media(max-width:1270px)]:grid-cols-3 [@media(max-width:730px)]:grid-cols-2 [@media(max-width:500px)]:grid-cols-1"
ref={appsContainerRef}>
{filteredApps.length
? filteredApps.map((app) => (
<AppCard key={app.name} app={app} searchText={searchText} credentials={app.credentials} />
))
: t("no_results")}
</div>
{filteredApps.length ? (
<div
className="grid gap-3 lg:grid-cols-4 [@media(max-width:1270px)]:grid-cols-3 [@media(max-width:730px)]:grid-cols-2 [@media(max-width:500px)]:grid-cols-1"
ref={appsContainerRef}>
{filteredApps.map((app) => (
<AppCard key={app.name} app={app} searchText={searchText} credentials={app.credentials} />
))}{" "}
</div>
) : (
<EmptyScreen
Icon={Icon.FiSearch}
headline={t("no_results")}
description={searchText ? searchText?.toString() : ""}
/>
)}
</div>
);
}

View File

@ -1,7 +1,7 @@
import Link from "next/link";
import { ArrowRight } from "react-feather";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon } from "@calcom/ui";
import { SkeletonText } from "../skeleton";
import Slider from "./Slider";
@ -44,7 +44,7 @@ export default function AppStoreCategories({
)}
<p className="text-sm text-gray-500">
{isLocaleReady ? t("number_apps", { count: category.count }) : <SkeletonText invisible />}{" "}
<ArrowRight className="inline-block h-4 w-4" />
<Icon.FiArrowRight className="inline-block h-4 w-4" />
</p>
</div>
</a>

View File

@ -1,7 +1,7 @@
import { MouseEvent, useState } from "react";
import { Icon } from "react-feather";
import classNames from "@calcom/lib/classNames";
import { SVGComponent } from "@calcom/types/SVGComponent";
const stylesByVariant = {
neutral: { background: "bg-gray-100 ", text: "!text-gray-800", hover: "hover:!bg-gray-200" },
@ -14,7 +14,7 @@ export type BannerProps = {
description?: string;
variant: keyof typeof stylesByVariant;
errorMessage?: string;
Icon?: Icon;
Icon?: SVGComponent;
onDismiss: (event: MouseEvent<HTMLElement, globalThis.MouseEvent>) => void;
onAction?: (event: MouseEvent<HTMLElement, globalThis.MouseEvent>) => void;
actionText?: string;

View File

@ -1,9 +1,9 @@
import "react-calendar/dist/Calendar.css";
import "react-date-picker/dist/DatePicker.css";
import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle";
import { Calendar } from "react-feather";
import classNames from "@calcom/lib/classNames";
import { Icon } from "@calcom/ui";
type Props = {
date: Date;
@ -22,7 +22,7 @@ const DatePicker = ({ minDate, disabled, date, onDatesChange, className }: Props
)}
calendarClassName="rounded-md"
clearIcon={null}
calendarIcon={<Calendar className="h-5 w-5 rounded-md text-gray-500" />}
calendarIcon={<Icon.FiCalendar className="h-5 w-5 rounded-md text-gray-500" />}
value={date}
minDate={minDate}
disabled={disabled}

View File

@ -5,7 +5,7 @@ import { Props } from "react-select";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Select from "./select";
import { Select } from "../../../components/form/select";
export type Option = {
value: string;

View File

@ -7,12 +7,5 @@ export {
} from "./radio-area";
export { default as Checkbox } from "../../../components/form/checkbox/Checkbox";
export { default as DatePicker } from "./DatePicker";
export {
default as Select,
SelectWithValidation,
SelectField,
getReactSelectProps,
OptionComponentWithIcon,
} from "./select";
export { default as FormStep } from "./FormStep";

View File

@ -1,5 +0,0 @@
import Select from "./Select";
export default Select;
export { SelectWithValidation, SelectField, getReactSelectProps } from "./Select";
export { OptionComponentWithIcon } from "./components";

View File

@ -23,18 +23,7 @@ export {
DropdownMenuTriggerItem,
} from "./Dropdown";
export { default as EmptyScreen } from "./EmptyScreen";
export {
Checkbox,
DatePicker,
FormStep,
getReactSelectProps,
Radio,
RadioGroup,
Select,
SelectField,
SelectWithValidation,
OptionComponentWithIcon,
} from "./form";
export { Checkbox, DatePicker, FormStep, Radio, RadioGroup } from "./form";
export { default as LinkIconButton } from "./LinkIconButton";
export { List, ListItem } from "./List";
export type { ListItemProps } from "./List";

View File

@ -54,9 +54,12 @@ export default function InstalledAppsLayout({
if (query.data?.items.length === 0) {
actualTabs = tabs.filter((tab) => tab.name !== InstalledAppVariants.payment);
}
return (
<Shell {...rest}>
<AppCategoryNavigation baseURL="/apps/installed" containerClassname="w-full xl:mx-5 xl:w-4/5 xl:pr-5">
<AppCategoryNavigation
baseURL="/apps/installed"
containerClassname="w-full xl:mx-5 xl:w-4/5 xl:max-w-2xl xl:pr-5">
{children}
</AppCategoryNavigation>
</Shell>

View File

@ -10,6 +10,7 @@ export interface NavTabProps {
className?: string;
sticky?: boolean;
linkProps?: VerticalTabItemProps["linkProps"];
itemClassname?: string;
}
const NavTabs = function ({ tabs, className = "", sticky, linkProps, ...props }: NavTabProps) {
@ -25,7 +26,7 @@ const NavTabs = function ({ tabs, className = "", sticky, linkProps, ...props }:
{sticky && <div className="pt-6" />}
{props.children}
{tabs.map((tab, idx) => (
<VerticalTabItem {...tab} key={idx} linkProps={linkProps} />
<VerticalTabItem {...tab} key={idx} linkProps={linkProps} className={props.itemClassname} />
))}
</nav>
);

View File

@ -1,7 +1,8 @@
import classNames from "classnames";
import { Check, Info } from "react-feather";
import toast from "react-hot-toast";
import { Icon } from "@calcom/ui";
export function showToast(message: string, variant: "success" | "warning" | "error") {
switch (variant) {
case "success":
@ -12,7 +13,7 @@ export function showToast(message: string, variant: "success" | "warning" | "err
"data-testid-toast-success bg-brand-500 mb-2 flex h-9 items-center space-x-2 rounded-md p-3 text-sm font-semibold text-white shadow-md",
t.visible && "animate-fade-in-up"
)}>
<Check className="h-4 w-4" />
<Icon.FiCheck className="h-4 w-4" />
<p>{message}</p>
</div>
),
@ -27,7 +28,7 @@ export function showToast(message: string, variant: "success" | "warning" | "err
"animate-fade-in-up mb-2 flex h-9 items-center space-x-2 rounded-md bg-red-100 p-3 text-sm font-semibold text-red-900 shadow-md",
t.visible && "animate-fade-in-up"
)}>
<Info className="h-4 w-4" />
<Icon.FiInfo className="h-4 w-4" />
<p>{message}</p>
</div>
),
@ -42,7 +43,7 @@ export function showToast(message: string, variant: "success" | "warning" | "err
"animate-fade-in-up bg-brand-500 mb-2 flex h-9 items-center space-x-2 rounded-md p-3 text-sm font-semibold text-white shadow-md",
t.visible && "animate-fade-in-up"
)}>
<Info className="h-4 w-4" />
<Icon.FiInfo className="h-4 w-4" />
<p>{message}</p>
</div>
),
@ -57,7 +58,7 @@ export function showToast(message: string, variant: "success" | "warning" | "err
"animate-fade-in-up bg-brand-500 mb-2 flex h-9 items-center space-x-2 rounded-md p-3 text-sm font-semibold text-white shadow-md",
t.visible && "animate-fade-in-up"
)}>
<Check className="h-4 w-4" />
<Icon.FiCheck className="h-4 w-4" />
<p>{message}</p>
</div>
),

View File

@ -1,7 +1,6 @@
import { components, GroupBase, Props, ValueContainerProps } from "react-select";
import { Select } from "../..";
import { Icon } from "../../..";
import { Icon, Select } from "../../..";
const LimitedChipsContainer = <Option, IsMulti extends boolean, Group extends GroupBase<Option>>({
children,

863
yarn.lock

File diff suppressed because it is too large Load Diff