diff --git a/apps/web/components/webhook/WebhookDialogForm.tsx b/apps/web/components/webhook/WebhookDialogForm.tsx deleted file mode 100644 index bbbc018241..0000000000 --- a/apps/web/components/webhook/WebhookDialogForm.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useState } from "react"; -import { Controller, useForm } from "react-hook-form"; - -import { useLocale } from "@lib/hooks/useLocale"; -import showToast from "@lib/notification"; -import { trpc } from "@lib/trpc"; -import { WEBHOOK_TRIGGER_EVENTS } from "@lib/webhooks/constants"; -import customTemplate, { hasTemplateIntegration } from "@lib/webhooks/integrationTemplate"; - -import { DialogFooter } from "@components/Dialog"; -import { FieldsetLegend, Form, InputGroupBox, TextArea, TextField } from "@components/form/fields"; -import Button from "@components/ui/Button"; -import Switch from "@components/ui/Switch"; -import { TWebhook } from "@components/webhook/WebhookListItem"; -import WebhookTestDisclosure from "@components/webhook/WebhookTestDisclosure"; - -export default function WebhookDialogForm(props: { - eventTypeId?: number; - defaultValues?: TWebhook; - handleClose: () => void; -}) { - const { t } = useLocale(); - const utils = trpc.useContext(); - const handleSubscriberUrlChange = (e) => { - form.setValue("subscriberUrl", e.target.value); - if (hasTemplateIntegration({ url: e.target.value })) { - setUseCustomPayloadTemplate(true); - form.setValue("payloadTemplate", customTemplate({ url: e.target.value })); - } - }; - const { - defaultValues = { - id: "", - eventTriggers: WEBHOOK_TRIGGER_EVENTS, - subscriberUrl: "", - active: true, - payloadTemplate: null, - } as Omit, - } = props; - - const [useCustomPayloadTemplate, setUseCustomPayloadTemplate] = useState(!!defaultValues.payloadTemplate); - - const form = useForm({ - defaultValues, - }); - return ( -
{ - const e = { ...event, eventTypeId: props.eventTypeId }; - if (!useCustomPayloadTemplate && event.payloadTemplate) { - event.payloadTemplate = null; - } - if (event.id) { - await utils.client.mutation("viewer.webhook.edit", e); - await utils.invalidateQueries(["viewer.webhook.list"]); - showToast(t("webhook_updated_successfully"), "success"); - } else { - await utils.client.mutation("viewer.webhook.create", e); - await utils.invalidateQueries(["viewer.webhook.list"]); - showToast(t("webhook_created_successfully"), "success"); - } - props.handleClose(); - }} - className="space-y-4"> - -
- - ( - { - form.setValue("active", isChecked); - }} - /> - )} - /> - -
- - -
- {t("event_triggers")} - - {WEBHOOK_TRIGGER_EVENTS.map((key) => ( - ( - { - const value = field.value; - const newValue = isChecked ? [...value, key] : value.filter((v) => v !== key); - - form.setValue("eventTriggers", newValue, { - shouldDirty: true, - }); - }} - /> - )} - /> - ))} - -
-
- {t("payload_template")} -
- - -
- {useCustomPayloadTemplate && ( - + + + + {team &&
} + {team && (
-
+
} + defaultValue={eventType.schedulingType} + render={() => ( + { + // FIXME: Better types are needed + formMethods.setValue("schedulingType", val as SchedulingType); + }} + /> + )} />
-
-
-
+
-
-
-
- {team &&
} - {team && ( -
-
-
- -
+
user.id.toString())} render={() => ( - { - // FIXME: Better types are needed - formMethods.setValue("schedulingType", val as SchedulingType); + { + formMethods.setValue( + "users", + options.map((user) => user.value) + ); }} + defaultValue={eventType.users.map(mapUserToValue)} + options={teamMembers.map(mapUserToValue)} + placeholder={t("add_attendees")} /> )} />
- -
-
- -
-
- user.id.toString())} - render={() => ( - { - formMethods.setValue( - "users", - options.map((user) => user.value) - ); - }} - defaultValue={eventType.users.map(mapUserToValue)} - options={teamMembers.map(mapUserToValue)} - placeholder={t("add_attendees")} - /> - )} - /> -
-
- )} - setAdvancedSettingsVisible(!advancedSettingsVisible)}> - <> - - - - {t("show_advanced_settings")} - - - - {/** - * Only display calendar selector if user has connected calendars AND if it's not - * a team event. Since we don't have logic to handle each attende calendar (for now). - * This will fallback to each user selected destination calendar. - */} - {!!connectedCalendarsQuery.data?.connectedCalendars.length && !team && ( -
-
- -
-
-
- ( - - )} - /> -
-
-
- )} -
-
- -
-
-
- -
-
-
- {eventType.isWeb3Active && ( -
-
- -
-
-
- { - - } -
-
-
- )} +
+ )} + setAdvancedSettingsVisible(!advancedSettingsVisible)}> + <> + + + + {t("show_advanced_settings")} + + + + {/** + * Only display calendar selector if user has connected calendars AND if it's not + * a team event. Since we don't have logic to handle each attende calendar (for now). + * This will fallback to each user selected destination calendar. + */} + {!!connectedCalendarsQuery.data?.connectedCalendars.length && !team && (
-
-
-
    - {customInputs.map((customInput: EventTypeCustomInput, idx: number) => ( -
  • -
    -
    -
    - - {t("label")}: {customInput.label} - -
    - {customInput.placeholder && ( -
    - - {t("placeholder")}: {customInput.placeholder} - -
    - )} -
    - - {t("type")}: {customInput.type} - -
    -
    - - {customInput.required ? t("required") : t("optional")} - -
    -
    -
    - - -
    -
    -
  • - ))} -
  • - -
  • -
-
-
- - ( - { - formMethods.setValue("requiresConfirmation", e?.target.checked); - }} - /> - )} - /> - - ( - { - formMethods.setValue("disableGuests", e?.target.checked); - }} - /> - )} - /> - -
- ( - { - formMethods.setValue("minimumBookingNotice", Number(e.target.value)); - }} - /> - )} - /> - -
-
-
{ - const slotIntervalOptions = [ - { - label: t("slot_interval_default"), - value: -1, - }, - ...[5, 10, 15, 20, 30, 45, 60].map((minutes) => ({ - label: minutes + " " + t("minutes"), - value: minutes, - })), - ]; - return ( - - -
- )} - {period.type === "RANGE" && ( -
- ( - { - formMethods.setValue("periodDates", { startDate, endDate }); - }} - /> - )} - /> -
- )} - {period.suffix ? ( -  {period.suffix} - ) : null} -
- ))} - - )} + )} +
+
+ +
+
+
+
- -
-
+
+ {eventType.isWeb3Active && ( +
- ( - { - const schedule = { - openingHours: val.openingHours, - dateOverrides: val.dateOverrides, - }; - // Updating internal state that would be sent on mutation - setAvailabilityState(schedule); - // Updating form values displayed, but this one doesn't reach form submit scope - formMethods.setValue("availability", schedule); - }} - setTimeZone={(timeZone) => { - formMethods.setValue("timeZone", timeZone); - setSelectedTimeZone(timeZone); - }} - timeZone={selectedTimeZone} - availability={availability.map((schedule) => ({ - ...schedule, - startTime: new Date(schedule.startTime), - endTime: new Date(schedule.endTime), - }))} +
+ { + - )} + } +
+
+
+ )} +
+
+ +
+
+
    + {customInputs.map((customInput: EventTypeCustomInput, idx: number) => ( +
  • +
    +
    +
    + + {t("label")}: {customInput.label} + +
    + {customInput.placeholder && ( +
    + + {t("placeholder")}: {customInput.placeholder} + +
    + )} +
    + + {t("type")}: {customInput.type} + +
    +
    + + {customInput.required ? t("required") : t("optional")} + +
    +
    +
    + + +
    +
    +
  • + ))} +
  • + +
  • +
+
+
+ + ( + { + formMethods.setValue("requiresConfirmation", e?.target.checked); + }} + /> + )} + /> + + ( + { + formMethods.setValue("disableGuests", e?.target.checked); + }} + /> + )} + /> + +
+ ( + { + formMethods.setValue("minimumBookingNotice", Number(e.target.value)); + }} + /> + )} + /> + +
+
+ +
+
+
+ { + const slotIntervalOptions = [ + { + label: t("slot_interval_default"), + value: -1, + }, + ...[5, 10, 15, 20, 30, 45, 60].map((minutes) => ({ + label: minutes + " " + t("minutes"), + value: minutes, + })), + ]; + return ( + + +
+ )} + {period.type === "RANGE" && ( +
+ ( + { + formMethods.setValue("periodDates", { startDate, endDate }); + }} + /> + )} + /> +
+ )} + {period.suffix ? ( +  {period.suffix} + ) : null} +
+ ))} + + )} + /> +
+
+ +
+ +
+
+ +
+
+ ( + { + const schedule = { + openingHours: val.openingHours, + dateOverrides: val.dateOverrides, + }; + // Updating internal state that would be sent on mutation + setAvailabilityState(schedule); + // Updating form values displayed, but this one doesn't reach form submit scope + formMethods.setValue("availability", schedule); + }} + setTimeZone={(timeZone) => { + formMethods.setValue("timeZone", timeZone); + setSelectedTimeZone(timeZone); + }} + timeZone={selectedTimeZone} + availability={availability.map((schedule) => ({ + ...schedule, + startTime: new Date(schedule.startTime), + endTime: new Date(schedule.endTime), + }))} + /> + )} + /> +
+
+ + {hasPaymentIntegration && ( + <> +
+
+
+ +
+ +
+
+
+
+
+
+ { + setRequirePayment(event.target.checked); + if (!event.target.checked) { + formMethods.setValue("price", 0); + } + }} + id="requirePayment" + name="requirePayment" + type="checkbox" + className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300" + defaultChecked={requirePayment} + /> +
+
+

+ {t("require_payment")} (0.5% +{" "} + + + {" "} + {t("commission_per_transaction")}) +

+
+
+
+
- -
+ {requirePayment && (
-
-
- { - setRequirePayment(event.target.checked); - if (!event.target.checked) { - formMethods.setValue("price", 0); - } - }} - id="requirePayment" - name="requirePayment" - type="checkbox" - className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300" - defaultChecked={requirePayment} - /> -
-
-

- {t("require_payment")} (0.5% +{" "} - - - {" "} - {t("commission_per_transaction")}) -

+
+ ( + { + field.onChange(e.target.valueAsNumber * 100); + }} + value={field.value > 0 ? field.value / 100 : 0} + /> + )} + /> +
+ + {new Intl.NumberFormat("en", { + style: "currency", + currency: currency, + maximumSignificantDigits: 1, + maximumFractionDigits: 0, + }) + .format(0) + .replace("0", "")} +
- {requirePayment && ( -
-
-
-
- ( - { - field.onChange(e.target.valueAsNumber * 100); - }} - value={field.value > 0 ? field.value / 100 : 0} - /> - )} - /> -
- - {new Intl.NumberFormat("en", { - style: "currency", - currency: currency, - maximumSignificantDigits: 1, - maximumFractionDigits: 0, - }) - .format(0) - .replace("0", "")} - -
-
-
-
-
- )} -
+ )}
- - )} - - - {/* )} */} - -
- - -
- -
+
+ + )} + + + {/* )} */} + +
+ + +
+
-
-
+
+
+
+ ( + { + formMethods.setValue("hidden", isChecked); + }} + label={t("hide_event_type")} + /> + )} + /> +
+
+ + + + + + + {t("delete")} + + + {t("delete_event_type_description")} + + +
+
+
+ + +
+
+
+ +
+
+ +
+

{t("this_input_will_shown_booking_this_event")}

+
+
+
+
{ + const newLocation = values.locationType; + + let details = {}; + if (newLocation === LocationType.InPerson) { + details = { address: values.locationAddress }; + } + + const existingIdx = formMethods + .getValues("locations") + .findIndex((loc) => values.locationType === loc.type); + if (existingIdx !== -1) { + const copy = formMethods.getValues("locations"); + copy[existingIdx] = { + ...formMethods.getValues("locations")[existingIdx], + ...details, + }; + formMethods.setValue("locations", copy); + } else { + formMethods.setValue( + "locations", + formMethods.getValues("locations").concat({ type: values.locationType, ...details }) + ); + } + + setShowLocationModal(false); + }}> ( - { - formMethods.setValue("hidden", isChecked); + name="locationType" + control={locationFormMethods.control} + render={() => ( + { - if (val) { - locationFormMethods.setValue("locationType", val.value); - setSelectedLocation(val); - } - }} - /> - )} - /> - -
- - -
- -
-
-
- a.id - b.id) || []} - render={() => ( - - -
-
-
- -
-
- -
-

- {t("this_input_will_shown_booking_this_event")} -

-
+ +
+ +
+ a.id - b.id) || []} + render={() => ( + + +
+
+
+ +
+
+ +
+

+ {t("this_input_will_shown_booking_this_event")} +

- { - const customInput: EventTypeCustomInput = { - id: -1, - eventTypeId: -1, - label: values.label, - placeholder: values.placeholder, - required: values.required, - type: values.type, - }; - - if (selectedCustomInput) { - selectedCustomInput.label = customInput.label; - selectedCustomInput.placeholder = customInput.placeholder; - selectedCustomInput.required = customInput.required; - selectedCustomInput.type = customInput.type; - } else { - setCustomInputs(customInputs.concat(customInput)); - formMethods.setValue( - "customInputs", - formMethods.getValues("customInputs").concat(customInput) - ); - } - setSelectedCustomInputModalOpen(false); - }} - onCancel={() => { - setSelectedCustomInputModalOpen(false); - }} - />
- -
- )} - /> - {isAdmin && } - + { + const customInput: EventTypeCustomInput = { + id: -1, + eventTypeId: -1, + label: values.label, + placeholder: values.placeholder, + required: values.required, + type: values.type, + }; + + if (selectedCustomInput) { + selectedCustomInput.label = customInput.label; + selectedCustomInput.placeholder = customInput.placeholder; + selectedCustomInput.required = customInput.required; + selectedCustomInput.type = customInput.type; + } else { + setCustomInputs(customInputs.concat(customInput)); + formMethods.setValue( + "customInputs", + formMethods.getValues("customInputs").concat(customInput) + ); + } + setSelectedCustomInputModalOpen(false); + }} + onCancel={() => { + setSelectedCustomInputModalOpen(false); + }} + /> +
+ + + )} + />
); @@ -1621,7 +1604,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => accepted: true, }, select: { - role: true, user: { select: userSelect, }, diff --git a/apps/web/pages/integrations/index.tsx b/apps/web/pages/integrations/index.tsx index ef7a64e8d9..c90deea2ed 100644 --- a/apps/web/pages/integrations/index.tsx +++ b/apps/web/pages/integrations/index.tsx @@ -1,6 +1,9 @@ +import { ChevronRightIcon, PencilAltIcon, SwitchHorizontalIcon, TrashIcon } from "@heroicons/react/outline"; import { ClipboardIcon } from "@heroicons/react/solid"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible"; import Image from "next/image"; import React, { useEffect, useState } from "react"; +import { Controller, useForm, useWatch } from "react-hook-form"; import { JSONObject } from "superjson/dist/types"; import { QueryCell } from "@lib/QueryCell"; @@ -8,12 +11,17 @@ import classNames from "@lib/classNames"; import { HttpError } from "@lib/core/http/error"; import { useLocale } from "@lib/hooks/useLocale"; import showToast from "@lib/notification"; -import { trpc } from "@lib/trpc"; +import { inferQueryOutput, trpc } from "@lib/trpc"; +import { WEBHOOK_TRIGGER_EVENTS } from "@lib/webhooks/constants"; import { ClientSuspense } from "@components/ClientSuspense"; +import { Dialog, DialogContent, DialogFooter, DialogTrigger } from "@components/Dialog"; import { List, ListItem, ListItemText, ListItemTitle } from "@components/List"; import Loader from "@components/Loader"; import Shell, { ShellSubHeading } from "@components/Shell"; +import { Tooltip } from "@components/Tooltip"; +import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; +import { FieldsetLegend, Form, InputGroupBox, TextField, TextArea } from "@components/form/fields"; import { CalendarListContainer } from "@components/integrations/CalendarListContainer"; import ConnectIntegration from "@components/integrations/ConnectIntegrations"; import DisconnectIntegration from "@components/integrations/DisconnectIntegration"; @@ -21,7 +29,367 @@ import IntegrationListItem from "@components/integrations/IntegrationListItem"; import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections"; import { Alert } from "@components/ui/Alert"; import Button from "@components/ui/Button"; -import WebhookListContainer from "@components/webhook/WebhookListContainer"; +import Switch from "@components/ui/Switch"; + +type TWebhook = inferQueryOutput<"viewer.webhook.list">[number]; + +function WebhookListItem(props: { webhook: TWebhook; onEditWebhook: () => void }) { + const { t } = useLocale(); + const utils = trpc.useContext(); + const deleteWebhook = trpc.useMutation("viewer.webhook.delete", { + async onSuccess() { + await utils.invalidateQueries(["viewer.webhook.list"]); + }, + }); + + return ( + +
+
+
+ + {props.webhook.subscriberUrl} + +
+
+ + {props.webhook.eventTriggers.map((eventTrigger, ind) => ( + + {t(`${eventTrigger.toLowerCase()}`)} + + ))} + +
+
+
+ + + + + + + + + + deleteWebhook.mutate({ id: props.webhook.id })}> + {t("delete_webhook_confirmation_message")} + + +
+
+
+ ); +} + +function WebhookTestDisclosure() { + const subscriberUrl: string = useWatch({ name: "subscriberUrl" }); + const payloadTemplate = useWatch({ name: "payloadTemplate" }) || null; + const { t } = useLocale(); + const [open, setOpen] = useState(false); + const mutation = trpc.useMutation("viewer.webhook.testTrigger", { + onError(err) { + showToast(err.message, "error"); + }, + }); + + return ( + setOpen(!open)}> + + + {t("webhook_test")} + + + +
+

{t("webhook_response")}

+ +
+
+ {!mutation.data && {t("no_data_yet")}} + {mutation.status === "success" && ( + <> +
+ {mutation.data.ok ? t("success") : t("failed")} +
+
{JSON.stringify(mutation.data, null, 4)}
+ + )} +
+
+
+
+ ); +} + +function WebhookDialogForm(props: { + // + defaultValues?: TWebhook; + handleClose: () => void; +}) { + const { t } = useLocale(); + const utils = trpc.useContext(); + const supportedWebhookIntegrationList = ["https://discord.com/api/webhooks/"]; + + const handleSubscriberUrlChange = (e) => { + form.setValue("subscriberUrl", e.target.value); + const ind = supportedWebhookIntegrationList.findIndex((integration) => { + return e.target.value.includes(integration); + }); + if (ind > -1) updateCustomTemplate(supportedWebhookIntegrationList[ind]); + }; + + const updateCustomTemplate = (webhookIntegration) => { + setUseCustomPayloadTemplate(true); + switch (webhookIntegration) { + case "https://discord.com/api/webhooks/": + form.setValue( + "payloadTemplate", + '{"content": "A new event has been scheduled","embeds": [{"color": 2697513,"fields": [{"name": "What","value": "{{title}} ({{type}})"},{"name": "When","value": "Start: {{startTime}} \\n End: {{endTime}} \\n Timezone: ({{organizer.timeZone}})"},{"name": "Who","value": "Organizer: {{organizer.name}} ({{organizer.email}}) \\n Booker: {{attendees.0.name}} ({{attendees.0.email}})" },{"name":"Description", "value":": {{description}}"},{"name":"Where","value":": {{location}} "}]}]}' + ); + } + }; + + const { + defaultValues = { + id: "", + eventTriggers: WEBHOOK_TRIGGER_EVENTS, + subscriberUrl: "", + active: true, + payloadTemplate: null, + } as Omit, + } = props; + + const [useCustomPayloadTemplate, setUseCustomPayloadTemplate] = useState(!!defaultValues.payloadTemplate); + + const form = useForm({ + defaultValues, + }); + return ( +
{ + if (!useCustomPayloadTemplate && event.payloadTemplate) { + event.payloadTemplate = null; + } + if (event.id) { + await utils.client.mutation("viewer.webhook.edit", event); + await utils.invalidateQueries(["viewer.webhook.list"]); + showToast(t("webhook_updated_successfully"), "success"); + } else { + await utils.client.mutation("viewer.webhook.create", event); + await utils.invalidateQueries(["viewer.webhook.list"]); + showToast(t("webhook_created_successfully"), "success"); + } + props.handleClose(); + }} + className="space-y-4"> + +
+ + ( + { + form.setValue("active", isChecked); + }} + /> + )} + /> + +
+ + +
+ {t("event_triggers")} + + {WEBHOOK_TRIGGER_EVENTS.map((key) => ( + ( + { + const value = field.value; + const newValue = isChecked ? [...value, key] : value.filter((v) => v !== key); + + form.setValue("eventTriggers", newValue, { + shouldDirty: true, + }); + }} + /> + )} + /> + ))} + +
+
+ {t("payload_template")} +
+ + +
+ {useCustomPayloadTemplate && ( +