diff --git a/apps/web/components/webhook/WebhookDialogForm.tsx b/apps/web/components/webhook/WebhookDialogForm.tsx new file mode 100644 index 00000000..bbbc0182 --- /dev/null +++ b/apps/web/components/webhook/WebhookDialogForm.tsx @@ -0,0 +1,162 @@ +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 && ( +
-
+
( - { - // FIXME: Better types are needed - formMethods.setValue("schedulingType", val as SchedulingType); - }} - /> - )} + defaultValue={eventType.locations || []} + render={() => } />
- +
+
+
-
-
+ {team &&
} + {team && ( +
+
+
+ +
user.id.toString())} + defaultValue={eventType.schedulingType} render={() => ( - { - formMethods.setValue( - "users", - options.map((user) => user.value) - ); + { + // FIXME: Better types are needed + formMethods.setValue("schedulingType", val as SchedulingType); }} - 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 && ( + )} + 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 && ( +
+
+ +
+
+
+ { + + } +
+
+
+ )}
+
+
+
    + {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)); + }} + /> + )} + /> + +
+
+
( - - )} + render={() => { + 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 ( + -
-
-
- {eventType.isWeb3Active && ( -
+ +
-
- { - - } -
-
-
- )} -
-
- -
-
-
    - {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), + }))} + /> + )} + /> +
-
- ( - - formMethods.setValue("periodType", val as PeriodType) - }> - {PERIOD_TYPES.map((period) => ( -
- - - - {period.prefix ? {period.prefix}  : null} - {period.type === "ROLLING" && ( -
- - -
- )} - {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")}) -

-
-
-
-
+ {hasPaymentIntegration && ( + <> +
+
+
+
- {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", "")} - +
+
+ { + 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 && ( +
+
+
+
+ ( + { + 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); - }}> +
+
+
( - { + if (val) { + locationFormMethods.setValue("locationType", val.value); + setSelectedLocation(val); + } + }} + /> + )} + /> + +
+ + +
+ +
+ +
+ 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 && } +
); @@ -1604,6 +1621,7 @@ 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 c90deea2..ef7a64e8 100644 --- a/apps/web/pages/integrations/index.tsx +++ b/apps/web/pages/integrations/index.tsx @@ -1,9 +1,6 @@ -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"; @@ -11,17 +8,12 @@ import classNames from "@lib/classNames"; import { HttpError } from "@lib/core/http/error"; import { useLocale } from "@lib/hooks/useLocale"; import showToast from "@lib/notification"; -import { inferQueryOutput, trpc } from "@lib/trpc"; -import { WEBHOOK_TRIGGER_EVENTS } from "@lib/webhooks/constants"; +import { trpc } from "@lib/trpc"; 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"; @@ -29,367 +21,7 @@ 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 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 && ( -