V2 settings - developer view (#4085)

* Fix breadcrumb colors

* HorizontalTabs

* Team List Item WIP

* Horizontal Tabs

* Cards

* Remove team list item WIP

* Login Page

* Add welcome back i118n

* EventType page work

* Update EventType Icons

* WIP Availability

* Horizontal Tab Work

* Add build command for in root

* Update build DIr/command

* Add Edit Button + change buttons to v2

* Availablitiy page

* Fix IPAD

* Make mobile look a little nicer

* WIP bookingshell

* Remove list items from breaking build

* Mian bulk of Booking Page.

* Few updates to components

* Fix chormatic feedback

* Fix banner

* Fix Empty Screen

* Text area + embded window fixes

* Semi fix avatar

* Troubleshoot container + Active on count

* Improve mobile

* NITS

* Fix padding on input

* Fix icons

* Starting to move event types settings to tabs

* Begin migration to single page form

* Single page tabs

* Limits Page

* Advanced tab

* Add RHF to dependancies

* Most of advanced tab

* Solved RHF mismtach

* Build fixes

* RHF conditionals fixes

* Improved legibility

* Major refactor/organisation into optional V2 UI

* Portal EditLocationModal

* Fix dialoug form

* Update imports

* Auto Animate + custom inputs WIP

* Custom Inputs

* WIP Apps

* Fixing stories imports

* Stripe app

* Remove duplicate dialog

* Remove duplicate dialog

* Fix embed URL

* Fix app toggles + number of active apps

* Fix container padding on disabledBorder prop

* Removes strict

* EventType Team page WIP

* Fix embed

* NIT

* Add Darkmode gray color

* V2 Shell WIP

* Create my account folder

* Add profile section

* Fix headings on shell V2

* Fix mobile layout with V2 shell

* V2 create event type button

* Checked Team Select

* Hidden to happen on save - not on toggle

* Team Attendee Select animation

* WIP

* Fix scheduling type and remove multi select label

* Fix overflow on teams url

* Finish profile fields

* Show toast on success

* General tab WIP

* Even Type move order handles

* Add switching of destination calendar

* List calendar and delete

* Render empty screenwhen no calendars

* Fix Embed TS errors

* Fix TS errors

* Fix Eslint errors

* Fix TS errors for UI

* Fix ESLINT error

* added SidebarCard for promo to v2 and storybook (#3906)

Co-authored-by: Julian Benegas <julianbenegas99@gmail.com>
Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

* Tooltip Provider - Wrapper due to dep upgrade

* public event type list darkmode

* V2 Color changes to public booking

* Remove unused component

* Fix typecheck

* Transfer to SSR

* Appearance screen made

* V2 image uploader

* WIP appearance page

* Remove unnecessary data from viewer.me

* Add profile translations

* Add translations to general page

* Add calendar switch

* Add calendar switch

* Add translations to appearance page

* Clean up conferencing page

* Clean up appearance page

* Draft shell on md screen

* fixed slots availability by fixing buffertime (#3756)

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* Hidding import features from other services (#3970)

* Update PULL_REQUEST_TEMPLATE.md

* Fixes CLS when loading Avatars (#3973)

* Fixes CLS when loading Avatars

* Update packages/ui/v2/core/Avatar.tsx

Co-authored-by: Leo Giovanetti <hello@leog.me>

Co-authored-by: Leo Giovanetti <hello@leog.me>

* New Crowdin translations by Github Action (#3954)

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>

* Adding continue button to connectCalendar on getting-started view  (#3971)

* Adding continue button to connectCalendar view getting-started

* Fixing extra space

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>

* 2.0 Settings / My Account {View} (#3874)

* Fix breadcrumb colors

* HorizontalTabs

* Team List Item WIP

* Horizontal Tabs

* Cards

* Remove team list item WIP

* Login Page

* Add welcome back i118n

* EventType page work

* Update EventType Icons

* WIP Availability

* Horizontal Tab Work

* Add build command for in root

* Update build DIr/command

* Add Edit Button + change buttons to v2

* Availablitiy page

* Fix IPAD

* Make mobile look a little nicer

* WIP bookingshell

* Remove list items from breaking build

* Mian bulk of Booking Page.

* Few updates to components

* Fix chormatic feedback

* Fix banner

* Fix Empty Screen

* Text area + embded window fixes

* Semi fix avatar

* Troubleshoot container + Active on count

* Improve mobile

* NITS

* Fix padding on input

* Fix icons

* Starting to move event types settings to tabs

* Begin migration to single page form

* Single page tabs

* Limits Page

* Advanced tab

* Add RHF to dependancies

* Most of advanced tab

* Solved RHF mismtach

* Build fixes

* RHF conditionals fixes

* Improved legibility

* Major refactor/organisation into optional V2 UI

* Portal EditLocationModal

* Fix dialoug form

* Update imports

* Auto Animate + custom inputs WIP

* Custom Inputs

* WIP Apps

* Fixing stories imports

* Stripe app

* Remove duplicate dialog

* Remove duplicate dialog

* Fix embed URL

* Fix app toggles + number of active apps

* Fix container padding on disabledBorder prop

* Removes strict

* EventType Team page WIP

* Fix embed

* NIT

* Add Darkmode gray color

* V2 Shell WIP

* Create my account folder

* Add profile section

* Fix headings on shell V2

* Fix mobile layout with V2 shell

* V2 create event type button

* Checked Team Select

* Hidden to happen on save - not on toggle

* Team Attendee Select animation

* WIP

* Fix scheduling type and remove multi select label

* Fix overflow on teams url

* Finish profile fields

* Show toast on success

* General tab WIP

* Even Type move order handles

* Add switching of destination calendar

* List calendar and delete

* Render empty screenwhen no calendars

* Fix Embed TS errors

* Fix TS errors

* Fix Eslint errors

* Fix TS errors for UI

* Fix ESLINT error

* added SidebarCard for promo to v2 and storybook (#3906)

Co-authored-by: Julian Benegas <julianbenegas99@gmail.com>
Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

* Tooltip Provider - Wrapper due to dep upgrade

* public event type list darkmode

* V2 Color changes to public booking

* Remove unused component

* Fix typecheck

* Transfer to SSR

* Appearance screen made

* V2 image uploader

* WIP appearance page

* Remove unnecessary data from viewer.me

* Add profile translations

* Add translations to general page

* Add calendar switch

* Add calendar switch

* Add translations to appearance page

* Clean up conferencing page

* Settings sidebar fixes

* Updates middleware

* Update SettingsLayout.tsx

* Settings layout improvements

* Type fix

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Julian Benegas <julianbenegas99@gmail.com>
Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* AppStore CLI: Making video app creation a breeze with major cleanup of locations code throughout (#3825)

* Fix breadcrumb colors

* HorizontalTabs

* Team List Item WIP

* Horizontal Tabs

* Cards

* Remove team list item WIP

* Login Page

* Add welcome back i118n

* EventType page work

* Update EventType Icons

* WIP Availability

* Horizontal Tab Work

* Add build command for in root

* Update build DIr/command

* Add Edit Button + change buttons to v2

* Availablitiy page

* Fix IPAD

* Make mobile look a little nicer

* WIP bookingshell

* Remove list items from breaking build

* Add Embed ModalBox for routing forms

* Mian bulk of Booking Page.

* Few updates to components

* Fix chormatic feedback

* Add duplicate form support

* Fix duplication logic

* Change to feathericons everywhere and other fixes

* Dont allow routes for fallback route

* Fix banner

* Fix Empty Screen

* Text area + embded window fixes

* Semi fix avatar

* Fix all TS issues

* Fix tests

* Troubleshoot container + Active on count

* Support routing using query params

* Improve mobile

* NITS

* Fix padding on input

* Support multiselect in router endpoint

* Fix the issue where app goes in embed mode after viewing embed once

* Fix icons

* Add router url tests

* Add Responses download and form toggling tests

* Add required validation test

* Change Icons everywhere

* App typeform app

* Improvements in cli

* Starting to move event types settings to tabs

* Begin migration to single page form

* Single page tabs

* Limits Page

* Advanced tab

* Add RHF to dependancies

* Add typeform how-to-use page

* Add typeform how-to-use page and screenshots

* Most of advanced tab

* Solved RHF mismtach

* Build fixes

* RHF conditionals fixes

* Improved legibility

* Fix TS error

* Add missing image

* Update CliApp.tsx

* Major refactor/organisation into optional V2 UI

* Portal EditLocationModal

* Fix dialoug form

* Update imports

* Auto Animate + custom inputs WIP

* Custom Inputs

* WIP Apps

* Fixing stories imports

* Stripe app

* Remove duplicate dialog

* Remove duplicate dialog

* Major locations cleanup, 10s of bug fixes and app-store improvements

* Fix missing pieces

* More fixes

* Fix embed URL

* Fix app toggles + number of active apps

* Fix container padding on disabledBorder prop

* Removes strict

* more fixes

* EventType Team page WIP

* Fix embed

* NIT

* Add Darkmode gray color

* V2 Shell WIP

* Fix headings on shell V2

* Fix mobile layout with V2 shell

* V2 create event type button

* Checked Team Select

* Hidden to happen on save - not on toggle

* Team Attendee Select animation

* Fix scheduling type and remove multi select label

* Fix overflow on teams url

* Revert console

* Revert api

* Fix Embed TS errors

* Fix TS errors

* Fix Eslint errors

* Fix TS errors for UI

* Fix ESLINT error

* Fix TS errors

* Add missing import

* Fix CLI

* Add a default placeholder

* Remove hardcoded daily:integrations

* Fix message for payment page

* Revert api and console to main

* Update README

* Fix TS errors

* Fix Lint warnings

* Fix Tests

* Fix conflict issues

* Fix conflict issues

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>

* Button to test a workflow action (#3873)

* add Test action button + UI improvements

* add test action functionality

* add confirmation dialog before sending SMS

* code clean up

* show error message if test action fails

* disable test action button in edit mode

* fixes SMS testing

* use updated values

* fix wrongly updated data in useEffect

* fix typo

* code clean up

* fix UI issue in mobile view

* small design fix

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* Improve CLI App Help Text (#3982)

* Adds deprecation notice to QueryCell (#3977)

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* added campfire.to app (#3984)

* fixed file size campfire (#3985)

* added campfire.to app

* fixed file size

* Draft shell on md screen

* Draft small settings bar

* Add padding to dropdown menu

* WIP

* Create mobile settings nav

* Fix conferencing render

* Only display mobile settings nav

* Clean up my account section

* Clean up

* Remove old draft shell

* Remove old draft shell

* Implement changes from #3817

* WIP

* Create webhooks page

* WIP Webhook form

* WIP

* WIP Webhook

* WIP

* Create edit webhook page

* Edit webhooks

* Edit API keys

* Update API key name

* Create embeds page

* Fix type errors

* Remove embeds page

* Improves Zapier response and payload (#3892)

* improve listBookings API call

* improve payload for zapier call

* use more readable location for integrations

* remove cancellation reason

* convert location to readbale string

* code clean up

* code clean up

* updated expected webhook response for e2e tests

* fix failing e2e

* revert webhook response

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* fixed dark mode team page (#4315)

* Checkly formatting

* Move hidden label to toggle on events page (#4320)

* Add events for routing forms

* Bring shell changes from main

* Clean up

* WIP

* V2 Multi-select (Team Select) (#4324)

* --init

* design improved

* further fine tuning

* more fixes

* removed extra JSX tag

* added story

* NIT

* revert to use of CheckedTeamSelect

* Removes comments

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* fix: toggle alligment (#4361)

* fix: add checked tranform for switch (#4357)

* fixed input size on mobile, fixed settings (#4360)

* fixed input size on mobile, fixed settings (#4360)

* Fix merge errors

* Fix API key page

* Fix type error

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Julian Benegas <julianbenegas99@gmail.com>
Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: Kszemi <mkrzemien11@gmail.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Noah <hi@noahflk.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
This commit is contained in:
Joe Au-Yeung
2022-09-12 14:07:52 -05:00
committed by GitHub
co-authored by sean-brydon Peer Richelsen zomars Hariom Balhara Julian Benegas Alan Kszemi Leo Giovanetti github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Crowdin Bot kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Carina Wollendorfer CarinaWolli Noah Syed Ali Shahbaz Udit Takkar
parent 27f58c70d3
commit ded1fce6fa
36 changed files with 1140 additions and 60 deletions
@@ -2,6 +2,7 @@ import { Webhook } from "@prisma/client";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import customTemplate, { hasTemplateIntegration } from "@calcom/features/webhooks/utils/integrationTemplate";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
@@ -11,11 +12,11 @@ import Switch from "@calcom/ui/Switch";
import { FieldsetLegend, Form, InputGroupBox, TextArea, TextField } from "@calcom/ui/form/fields";
import { WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP } from "@lib/webhooks/constants";
import customTemplate, { hasTemplateIntegration } from "@lib/webhooks/integrationTemplate";
import { TWebhook } from "@components/webhook/WebhookListItem";
import WebhookTestDisclosure from "@components/webhook/WebhookTestDisclosure";
/** @deprecated Moved to `packages/features/webhooks` */
export default function WebhookDialogForm(props: {
eventTypeId?: number;
defaultValues?: TWebhook;
@@ -21,6 +21,7 @@ export type WebhookListContainerType = {
appId?: string;
};
/** @deprecated Moved to `packages/features/webhooks` */
export default function WebhookListContainer(props: WebhookListContainerType) {
const router = useRouter();
const query = trpc.useQuery(
@@ -11,6 +11,7 @@ import { useLocale } from "@lib/hooks/useLocale";
export type TWebhook = inferQueryOutput<"viewer.webhook.list">[number];
/** @deprecated Moved to `packages/features/webhooks` */
export default function WebhookListItem(props: { webhook: TWebhook; onEditWebhook: () => void }) {
const { t } = useLocale();
const utils = trpc.useContext();
@@ -11,6 +11,7 @@ import { InputGroupBox } from "@calcom/ui/form/fields";
import { useLocale } from "@lib/hooks/useLocale";
/** @deprecated Moved to `packages/features/webhooks` */
export default function WebhookTestDisclosure() {
const subscriberUrl: string = useWatch({ name: "subscriberUrl" });
const payloadTemplate = useWatch({ name: "payloadTemplate" }) || null;
-2
View File
@@ -5,8 +5,6 @@ import { compile } from "handlebars";
import { getHumanReadableLocationValue } from "@calcom/app-store/locations";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { getTranslation } from "@server/lib/i18n";
type ContentType = "application/json" | "application/x-www-form-urlencoded";
export type EventTypeInfo = {
+2
View File
@@ -7,6 +7,8 @@ import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry
const V2_WHITELIST = [
"/settings/admin",
"/settings/developer/webhooks",
"/settings/developer/api-keys",
"/settings/my-account",
"/settings/security",
"/availability",
+3 -3
View File
@@ -21,6 +21,7 @@ import {
sendScheduledSeatsEmails,
} from "@calcom/emails";
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import getWebhooks from "@calcom/features/webhooks/utils/getWebhooks";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
import { getErrorFromUnknown } from "@calcom/lib/errors";
@@ -28,7 +29,6 @@ import isOutOfBounds, { BookingDateInPastError } from "@calcom/lib/isOutOfBounds
import logger from "@calcom/lib/logger";
import { defaultResponder, getLuckyUser } from "@calcom/lib/server";
import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager";
import getSubscribers from "@calcom/lib/webhooks/subscriptions";
import prisma, { userSelect } from "@calcom/prisma";
import { extendedBookingCreateBody, requiredCustomInputSchema } from "@calcom/prisma/zod-utils";
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
@@ -827,7 +827,7 @@ async function handler(req: NextApiRequest) {
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
};
const subscribersMeetingEnded = await getSubscribers(subscriberOptionsMeetingEnded);
const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded);
subscribersMeetingEnded.forEach((subscriber) => {
if (rescheduleUid && originalRescheduledBooking) {
@@ -839,7 +839,7 @@ async function handler(req: NextApiRequest) {
});
// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED
const subscribers = await getSubscribers(subscriberOptions);
const subscribers = await getWebhooks(subscriberOptions);
console.log("evt:", {
...evt,
metadata: reqBody.metadata,
@@ -0,0 +1,88 @@
import { useState } from "react";
import { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem";
import LicenseRequired from "@calcom/ee/common/components/v2/LicenseRequired";
import ApiKeyDialogForm from "@calcom/features/ee/api-keys/components/v2/ApiKeyDialogForm";
import ApiKeyListItem from "@calcom/features/ee/api-keys/components/v2/ApiKeyListItem";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui";
import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader";
import { EmptyScreen, Button } from "@calcom/ui/v2";
import { Dialog, DialogContent } from "@calcom/ui/v2/core/Dialog";
import Meta from "@calcom/ui/v2/core/Meta";
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
const ApiKeysView = () => {
const { t } = useLocale();
const { data, isLoading } = trpc.useQuery(["viewer.apiKeys.list"]);
const [apiKeyModal, setApiKeyModal] = useState(false);
const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires?: boolean }) | undefined>(
undefined
);
const NewApiKeyButton = () => {
return (
<Button
color="secondary"
StartIcon={Icon.FiPlus}
onClick={() => {
setApiKeyToEdit(undefined);
setApiKeyModal(true);
}}>
{t("new_api_key")}
</Button>
);
};
return (
<>
<Meta title="api_keys" description="webhooks_description" />
<LicenseRequired>
<>
{isLoading && <SkeletonLoader />}
<div>
{data?.length ? (
<>
<div className="mt-6 mb-8 rounded-md border">
{data?.map((apiKey, index) => (
<ApiKeyListItem
key={apiKey.id}
apiKey={apiKey}
lastItem={data.length === index + 1}
onEditClick={() => {
setApiKeyToEdit(apiKey);
setApiKeyModal(true);
}}
/>
))}
</div>
<NewApiKeyButton />
</>
) : (
<EmptyScreen
Icon={Icon.FiLink}
headline={t("create_first_api_key")}
description={t("create_first_api_key_description")}
buttonRaw={<NewApiKeyButton />}
/>
)}
</div>
</>
</LicenseRequired>
<Dialog open={apiKeyModal} onOpenChange={setApiKeyModal}>
<DialogContent type="creation" useOwnActionButtons>
<ApiKeyDialogForm handleClose={() => setApiKeyModal(false)} defaultValues={apiKeyToEdit} />
</DialogContent>
</Dialog>
</>
);
};
ApiKeysView.getLayout = getLayout;
export default ApiKeysView;
@@ -0,0 +1 @@
export { default } from "@calcom/features/webhooks/pages/webhook-edit-view";
@@ -0,0 +1 @@
export { default } from "@calcom/features/webhooks/pages/webhooks-view";
@@ -0,0 +1 @@
export { default } from "@calcom/features/webhooks/pages/webhook-new-view";
+21 -2
View File
@@ -352,10 +352,10 @@
"booking_cancelled": "Booking Cancelled",
"booking_rescheduled": "Booking Rescheduled",
"booking_created": "Booking Created",
"meeting_ended": "Meeting ended",
"meeting_ended": "Meeting Ended",
"form_submitted": "Form Submitted",
"event_triggers": "Event Triggers",
"subscriber_url": "Subscriber Url",
"subscriber_url": "Subscriber URL",
"create_new_webhook": "Create a new webhook",
"webhooks": "Webhooks",
"team_webhooks": "Team Webhooks",
@@ -1166,6 +1166,8 @@
"download_responses": "Download Responses",
"create_your_first_form": "Create your first form",
"create_your_first_form_description": "With Routing Forms you can ask qualifying questions and route to the correct person or event type.",
"create_your_first_webhook": "Create your first Webhook",
"create_your_first_webhook_description": "With Webhooks you can receive meeting data in real-time when something happens in Cal.com.",
"for_a_maximum_of": "For a maximum of",
"event_one": "event",
"event_other": "events",
@@ -1187,6 +1189,23 @@
"add_new_form": "Add new form",
"form_description": "Create your form to route a booker",
"copy_link_to_form": "Copy link to form",
"add_webhook_description": "Receive meeting data in real-time when something happens in Cal.com",
"triggers_when": "Triggers when",
"test_webhook": "Please ping test before creating.",
"enable_webhook": "Enable Webhook",
"add_webhook": "Add Webhook",
"webhook_edited_successfully": "Webhook saved",
"webhooks_description": "Receive meeting data in real-time when something happens in Cal.com",
"api_keys_description": "Generate API keys for accessing your own account",
"new_api_key": "New API key",
"active": "Active",
"api_key_updated": "API key name updated",
"api_key_update_failed": "Error updating API key name",
"embeds_title": "HTML iframe embed",
"embeds_description": "Embed all your event types on your website",
"pending_payment": "Pending payment",
"create_first_api_key": "Create your first API key",
"create_first_api_key_description": "API keys allow other apps to communicate with Cal.com",
"back_to_signin": "Back to sign in",
"reset_link_sent": "Reset link sent",
"password_reset_email":"An email is on its way to {{email}} with instructions to reset your password.",
@@ -2,8 +2,8 @@ import { Prisma, WebhookTriggerEvents } from "@prisma/client";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import getWebhooks from "@calcom/features/webhooks/utils/getWebhooks";
import { sendGenericWebhookPayload } from "@calcom/lib/webhooks/sendPayload";
import getWebhooks from "@calcom/lib/webhooks/subscriptions";
import { TRPCError } from "@calcom/trpc/server";
import { createProtectedRouter, createRouter } from "@calcom/trpc/server/createRouter";
@@ -0,0 +1,182 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import dayjs from "@calcom/dayjs";
import { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem";
import LicenseRequired from "@calcom/ee/common/components/v2/LicenseRequired";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { DialogFooter } from "@calcom/ui/Dialog";
import { ClipboardCopyIcon } from "@calcom/ui/Icon";
import { Tooltip } from "@calcom/ui/Tooltip";
import { DatePicker } from "@calcom/ui/v2";
import Button from "@calcom/ui/v2/core/Button";
import Switch from "@calcom/ui/v2/core/Switch";
import { Form, TextField } from "@calcom/ui/v2/core/form/fields";
export default function ApiKeyDialogForm({
defaultValues,
handleClose,
}: {
defaultValues?: Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires?: boolean };
handleClose: () => void;
}) {
const { t } = useLocale();
const utils = trpc.useContext();
const updateApiKeyMutation = trpc.useMutation("viewer.apiKeys.edit", {
onSuccess() {
utils.invalidateQueries("viewer.apiKeys.list");
showToast(t("api_key_updated"), "success");
handleClose();
},
onError() {
showToast(t("api_key_update_failed"), "error");
},
});
const [apiKey, setApiKey] = useState("");
const [successfulNewApiKeyModal, setSuccessfulNewApiKeyModal] = useState(false);
const [apiKeyDetails, setApiKeyDetails] = useState({
expiresAt: null as Date | null,
note: "" as string | null,
neverExpires: false,
});
const form = useForm({
defaultValues: {
note: defaultValues?.note || "",
neverExpires: defaultValues?.neverExpires || false,
expiresAt: defaultValues?.expiresAt || dayjs().add(1, "month").toDate(),
},
});
const watchNeverExpires = form.watch("neverExpires");
return (
<LicenseRequired>
{successfulNewApiKeyModal ? (
<>
<div className="mb-10">
<h2 className="font-semi-bold font-cal mb-2 text-xl tracking-wide text-gray-900">
{t("success_api_key_created")}
</h2>
<div className="text-sm text-gray-900">
<span className="font-semibold">{t("success_api_key_created_bold_tagline")}</span>{" "}
{t("you_will_only_view_it_once")}
</div>
</div>
<div>
<div className="flex">
<code className="my-2 mr-1 w-full truncate rounded-sm bg-gray-100 py-2 px-3 align-middle font-mono text-gray-800">
{apiKey}
</code>
<Tooltip side="top" content={t("copy_to_clipboard")}>
<Button
onClick={() => {
navigator.clipboard.writeText(apiKey);
showToast(t("api_key_copied"), "success");
}}
type="button"
className=" my-2 px-4 text-base">
<ClipboardCopyIcon className="mr-2 h-5 w-5 text-neutral-100" />
{t("copy")}
</Button>
</Tooltip>
</div>
<span className="text-sm text-gray-400">
{apiKeyDetails.neverExpires
? t("never_expire_key")
: `${t("expires")} ${apiKeyDetails?.expiresAt?.toLocaleDateString()}`}
</span>
</div>
<DialogFooter>
<Button type="button" color="secondary" onClick={handleClose} tabIndex={-1}>
{t("done")}
</Button>
</DialogFooter>
</>
) : (
<Form
form={form}
handleSubmit={async (event) => {
if (defaultValues) {
console.log("Name changed");
await updateApiKeyMutation.mutate({ id: defaultValues.id, note: event.note });
} else {
const apiKey = await utils.client.mutation("viewer.apiKeys.create", event);
setApiKey(apiKey);
setApiKeyDetails({ ...event });
await utils.invalidateQueries(["viewer.apiKeys.list"]);
setSuccessfulNewApiKeyModal(true);
}
}}
className="space-y-4">
<div className="mb-10 mt-1">
<h2 className="font-semi-bold font-cal text-xl tracking-wide text-gray-900">
{defaultValues ? t("edit_api_key") : t("create_api_key")}
</h2>
<p className="mt-1 mb-5 text-sm text-gray-500">{t("api_key_modal_subtitle")}</p>
</div>
<div>
<Controller
name="note"
control={form.control}
render={({ field: { onChange, value } }) => (
<TextField
name="note"
label={t("personal_note")}
placeholder={t("personal_note_placeholder")}
value={value}
onChange={(e) => {
form.setValue("note", e?.target.value);
}}
type="text"
/>
)}
/>
</div>
{!defaultValues && (
<div className="flex flex-col">
<div className="flex justify-between py-2">
<span className="block text-sm font-medium text-gray-700">{t("expire_date")}</span>
<Controller
name="neverExpires"
control={form.control}
render={({ field: { onChange, value } }) => (
<Switch
label={t("never_expire_key")}
onCheckedChange={onChange}
checked={value}
disabled={!!defaultValues}
/>
)}
/>
</div>
<Controller
name="expiresAt"
render={({ field: { onChange, value } }) => (
<DatePicker
disabled={watchNeverExpires || !!defaultValues}
minDate={new Date()}
date={value}
onDatesChange={onChange}
/>
)}
/>
</div>
)}
<DialogFooter>
<Button type="button" color="secondary" onClick={handleClose} tabIndex={-1}>
{t("cancel")}
</Button>
<Button type="submit" loading={form.formState.isSubmitting}>
{apiKeyDetails ? t("save") : t("create")}
</Button>
</DialogFooter>
</Form>
)}
</LicenseRequired>
);
}
@@ -0,0 +1,92 @@
import dayjs from "@calcom/dayjs";
import { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui";
import Badge from "@calcom/ui/v2/core/Badge";
import Button from "@calcom/ui/v2/core/Button";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@calcom/ui/v2/core/Dropdown";
const ApiKeyListItem = ({
apiKey,
lastItem,
onEditClick,
}: {
apiKey: TApiKeys;
lastItem: boolean;
onEditClick: () => void;
}) => {
const { t } = useLocale();
const utils = trpc.useContext();
const isExpired = apiKey?.expiresAt ? apiKey.expiresAt < new Date() : null;
const neverExpires = apiKey?.expiresAt === null;
const deleteApiKey = trpc.useMutation("viewer.apiKeys.delete", {
async onSuccess() {
await utils.invalidateQueries(["viewer.apiKeys.list"]);
},
});
return (
<div
key={apiKey.id}
className={classNames("flex w-full justify-between p-4", lastItem ? "" : "border-b")}>
<div>
<p className="font-medium"> {apiKey?.note ? apiKey.note : t("api_key_no_note")}</p>
<div className="flex items-center space-x-3.5">
{!neverExpires && isExpired && (
<Badge className="-p-2" variant="red">
{t("expired")}
</Badge>
)}
{!isExpired && (
<Badge className="-p-2" variant="green">
{t("active")}
</Badge>
)}
<p className="text-gray-600">
{" "}
{neverExpires ? (
<div className="flex flex-row space-x-3 text-gray-500">{t("api_key_never_expires")}</div>
) : (
`${isExpired ? t("expired") : t("expires")} ${dayjs(apiKey?.expiresAt?.toString()).fromNow()}`
)}
</p>
</div>
</div>
<div>
<Dropdown>
<DropdownMenuTrigger asChild>
<Button size="icon" StartIcon={Icon.FiMoreHorizontal} color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="flex items-center space-x-2.5 py-3 pr-32 pl-3.5"
onClick={onEditClick}>
<Icon.FiEdit2 />
<p>{t("edit")}</p>
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center space-x-2.5 py-3 pr-32 pl-3.5"
onClick={() =>
deleteApiKey.mutate({
id: apiKey.id,
})
}>
<Icon.FiTrash2 />
<p>{t("delete")}</p>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div>
</div>
);
};
export default ApiKeyListItem;
@@ -0,0 +1,266 @@
import { WebhookTriggerEvents } from "@prisma/client";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { classNames } from "@calcom/lib";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput } from "@calcom/trpc/react";
import Button from "@calcom/ui/v2/core/Button";
import Switch from "@calcom/ui/v2/core/Switch";
import Select from "@calcom/ui/v2/core/form/Select";
import { Form, Label, TextArea, TextField } from "@calcom/ui/v2/core/form/fields";
import customTemplate, { hasTemplateIntegration } from "../utils/integrationTemplate";
import WebhookTestDisclosure from "./WebhookTestDisclosure";
export type TWebhook = inferQueryOutput<"viewer.webhook.list">[number];
export type WebhookFormData = {
id?: string;
subscriberUrl: string;
active: boolean;
eventTriggers: WebhookTriggerEvents[];
secret: string | null;
payloadTemplate: string | undefined | null;
};
export type WebhookFormSubmitData = WebhookFormData & {
changeSecret: boolean;
newSecret: string;
};
type WebhookTriggerEventOptions = { value: WebhookTriggerEvents; label: string }[];
const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2: Record<string, WebhookTriggerEventOptions> = {
core: [
{ value: WebhookTriggerEvents.BOOKING_CANCELLED, label: "booking_cancelled" },
{ value: WebhookTriggerEvents.BOOKING_CREATED, label: "booking_created" },
{ value: WebhookTriggerEvents.BOOKING_RESCHEDULED, label: "booking_rescheduled" },
{ value: WebhookTriggerEvents.MEETING_ENDED, label: "meeting_ended" },
],
routing_forms: [{ value: WebhookTriggerEvents.FORM_SUBMITTED, label: "form_submitted" }],
};
const WebhookForm = (props: {
webhook?: WebhookFormData;
apps?: (keyof typeof WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2)[];
onSubmit: (event: WebhookFormSubmitData) => void;
}) => {
const { t } = useLocale();
const triggerOptions: WebhookTriggerEventOptions = WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2["core"];
if (props.apps) {
for (const app of props.apps) {
triggerOptions.push(...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2[app]);
}
}
const translatedTriggerOptions = triggerOptions.map((option) => ({ ...option, label: t(option.label) }));
const formMethods = useForm({
defaultValues: {
subscriberUrl: props?.webhook?.subscriberUrl || "",
active: props?.webhook?.active || false,
eventTriggers: props?.webhook?.eventTriggers || [],
secret: props?.webhook?.secret || "",
payloadTemplate: props?.webhook?.payloadTemplate || undefined,
},
});
const [useCustomTemplate, setUseCustomTemplate] = useState(false);
const [newSecret, setNewSecret] = useState("");
const [changeSecret, setChangeSecret] = useState(false);
const hasSecretKey = !!props?.webhook?.secret;
// const currentSecret = props?.webhook?.secret;
useEffect(() => {
if (changeSecret) {
formMethods.unregister("secret", { keepDefaultValue: false });
}
}, [changeSecret]);
return (
<>
<Form
form={formMethods}
handleSubmit={(values) => props.onSubmit({ ...values, changeSecret, newSecret })}>
<Controller
name="subscriberUrl"
control={formMethods.control}
render={({ field: { value } }) => (
<>
<TextField
name="subscriberUrl"
label={t("subscriber_url")}
labelClassName="font-medium text-gray-900 font-sm"
value={value}
onChange={(e) => {
formMethods.setValue("subscriberUrl", e?.target.value);
if (hasTemplateIntegration({ url: e.target.value })) {
setUseCustomTemplate(true);
formMethods.setValue("payloadTemplate", customTemplate({ url: e.target.value }));
}
}}
/>
</>
)}
/>
<Controller
name="active"
control={formMethods.control}
render={({ field: { value } }) => (
<div className="font-sm mt-8 font-medium text-gray-900">
<Switch
label={t("enable_webhook")}
checked={value}
// defaultChecked={props?.webhook?.active ? props?.webhook?.active : true}
onCheckedChange={(value) => {
formMethods.setValue("active", value);
}}
/>
</div>
)}
/>
<Controller
name="eventTriggers"
control={formMethods.control}
render={({ field: { onChange } }) => (
<div className="mt-8">
<Label className="font-sm mt-8 font-medium text-gray-900">
<>{t("event_triggers")}</>
</Label>
<Select
options={translatedTriggerOptions}
isMulti
onChange={(event) => {
onChange(event.map((selection) => selection.value));
}}
/>
</div>
)}
/>
<Controller
name="secret"
control={formMethods.control}
render={({ field: { value } }) => (
<div className="mt-8 ">
{!!hasSecretKey && !changeSecret && (
<>
<Label className="font-sm font-medium text-gray-900">Secret</Label>
<div className="space-y-0 rounded-md border-0 border-neutral-200 bg-white sm:mx-0 md:border">
<div className="rounded-sm border-b p-2 text-sm text-neutral-900">
{t("forgotten_secret_description")}
</div>
<div className="p-2">
<Button
color="secondary"
type="button"
onClick={() => {
setChangeSecret(true);
}}>
{t("change_secret")}
</Button>
</div>
</div>
</>
)}
{!!hasSecretKey && changeSecret && (
<>
<TextField
autoComplete="off"
label={t("secret")}
labelClassName="font-medium text-gray-900 font-sm"
{...formMethods.register("secret")}
value={newSecret}
onChange={(event) => setNewSecret(event.currentTarget.value)}
type="text"
placeholder={t("leave_blank_to_remove_secret")}
/>
<Button
color="secondary"
type="button"
className="py-1 text-xs"
onClick={() => {
setChangeSecret(false);
}}>
{t("cancel")}
</Button>
</>
)}
{!hasSecretKey && (
<TextField
name="secret"
label={t("secret")}
labelClassName="font-medium text-gray-900 font-sm"
value={value}
onChange={(e) => {
formMethods.setValue("secret", e?.target.value);
}}
/>
)}
</div>
)}
/>
<Controller
name="payloadTemplate"
control={formMethods.control}
render={({ field: { value } }) => (
<>
<Label className="font-sm mt-8 text-gray-900">
<>{t("event_triggers")}</>
</Label>
<div className="flex rounded-md border">
<div
className={classNames(
"px-1/2 w-1/2 rounded-md py-2.5 text-center font-medium text-gray-900",
!useCustomTemplate && "bg-gray-200"
)}
onClick={() => {
setUseCustomTemplate(false);
formMethods.setValue("payloadTemplate", undefined);
}}>
<p>{t("default")}</p>
</div>
<div
className={classNames(
"px-1/2 w-1/2 rounded-md py-2.5 text-center font-medium text-gray-900",
useCustomTemplate && "bg-gray-200"
)}
onClick={() => setUseCustomTemplate(true)}>
<p>{t("custom")}</p>
</div>
</div>
{useCustomTemplate && (
<TextArea
name="customPayloadTemplate"
rows={3}
value={value}
onChange={(e) => {
formMethods.setValue("payloadTemplate", e?.target.value);
}}
/>
)}
</>
)}
/>
<div className="mt-8">
<p className="font-medium text-black">{t("webhook_test")}</p>
<p className="font-sm mb-4 text-gray-600">{t("test_webhook")}</p>
<WebhookTestDisclosure />
</div>
<div className="mt-12 flex place-content-end space-x-4">
<Button type="button" color="minimal" href={`${WEBAPP_URL}/v2/settings/developer/webhooks`}>
{t("cancel")}
</Button>
<Button type="submit" loading={formMethods.formState.isSubmitting}>
{props?.webhook?.id ? t("save") : t("create")}
</Button>
</div>
</Form>
</>
);
};
export default WebhookForm;
@@ -0,0 +1,78 @@
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui/Icon";
import Badge from "@calcom/ui/v2/core/Badge";
import Button from "@calcom/ui/v2/core/Button";
import Switch from "@calcom/ui/v2/core/Switch";
import { Tooltip } from "@calcom/ui/v2/core/Tooltip";
export type TWebhook = inferQueryOutput<"viewer.webhook.list">[number];
export default function WebhookListItem(props: {
webhook: TWebhook;
onEditWebhook: () => void;
lastItem: boolean;
}) {
const { t } = useLocale();
const utils = trpc.useContext();
const { webhook } = props;
const deleteWebhook = trpc.useMutation("viewer.webhook.delete", {
async onSuccess() {
await utils.invalidateQueries(["viewer.webhook.list"]);
// TODO: Better success message
showToast(t("deleted"), "success");
},
});
const toggleWebhook = trpc.useMutation("viewer.webhook.edit", {
async onSuccess(data) {
console.log("data", data);
await utils.invalidateQueries(["viewer.webhook.list"]);
// TODO: Better success message
showToast(t(data?.active ? "enabled" : "disabled"), "success");
},
});
return (
<div className={classNames("flex w-full justify-between p-4", props.lastItem ? "" : "border-b")}>
<div>
<p className="text-sm font-medium text-gray-900">{webhook.subscriberUrl}</p>
<Tooltip
content={webhook.eventTriggers.map((trigger) => (
<p key={trigger}>{t(`${trigger.toLowerCase()}`)}</p>
))}>
<div className="mt-2.5 w-max">
<Badge variant="gray" bold StartIcon={Icon.FiAlertCircle}>
{t("triggers_when")}
</Badge>
</div>
</Tooltip>
</div>
<div className="flex items-center space-x-4">
<Switch
defaultChecked={webhook.active}
onCheckedChange={() =>
toggleWebhook.mutate({
id: webhook.id,
active: !webhook.active,
payloadTemplate: webhook.payloadTemplate,
})
}
/>
<Button color="secondary" onClick={props.onEditWebhook}>
{t("edit")}
</Button>
<Button
color="destructive"
StartIcon={Icon.FiTrash}
size="icon"
onClick={() => {
// TODO: Confimation dialog before deleting
deleteWebhook.mutate({ id: webhook.id });
}}
/>
</div>
</div>
);
}
@@ -0,0 +1,19 @@
import { SkeletonText } from "@calcom/ui/v2";
export default function WebhookListItemSkeleton() {
return (
<div className="flex w-full justify-between p-4">
<div>
<p className="text-sm font-medium text-gray-900">
<SkeletonText className="h-4 w-56" />
</p>
<div className="mt-2.5 w-max">
<SkeletonText className="h-5 w-28" />
</div>
</div>
<div className="flex items-center space-x-4">
<SkeletonText className="h-9 w-9" />
</div>
</div>
);
}
@@ -0,0 +1,15 @@
import { SkeletonContainer } from "@calcom/ui/v2";
import WebhookListItemSkeleton from "./WebhookListItemSkeleton";
export default function WebhookListSkeleton() {
return (
<SkeletonContainer>
<div className="mt-6 mb-8 divide-y rounded-md border">
<WebhookListItemSkeleton />
<WebhookListItemSkeleton />
<WebhookListItemSkeleton />
</div>
</SkeletonContainer>
);
}
@@ -0,0 +1,48 @@
import { useWatch } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui/Icon";
import Badge from "@calcom/ui/v2/core/Badge";
import Button from "@calcom/ui/v2/core/Button";
export default function WebhookTestDisclosure() {
const subscriberUrl: string = useWatch({ name: "subscriberUrl" });
const payloadTemplate = useWatch({ name: "payloadTemplate" }) || null;
const { t } = useLocale();
const mutation = trpc.useMutation("viewer.webhook.testTrigger", {
onError(err) {
showToast(err.message, "error");
},
});
return (
<div className="space-y-0 rounded-md border-0 border-neutral-200 bg-white sm:mx-0 md:border">
<div className="flex justify-between border-b p-4">
<div className="flex items-center space-x-1">
<h3 className="font-sm self-center font-medium text-black">{t("webhook_response")}</h3>
{mutation.data && (
<Badge variant={mutation.data.ok ? "green" : "red"}>
{mutation.data.ok ? t("passed") : t("failed")}
</Badge>
)}
</div>
<Button
type="button"
color="secondary"
disabled={mutation.isLoading}
StartIcon={Icon.FiActivity}
onClick={() => mutation.mutate({ url: subscriberUrl, type: "PING", payloadTemplate })}>
{t("ping_test")}
</Button>
</div>
<div className="p-4">
{!mutation.data && <em>{t("no_data_yet")}</em>}
{mutation.status === "success" && (
<div className="overflow-x-auto text-gray-900">{JSON.stringify(mutation.data, null, 4)}</div>
)}
</div>
</div>
);
}
@@ -0,0 +1,5 @@
export { default as WebhookForm } from "./WebhookForm";
export { default as WebhookListItem } from "./WebhookListItem";
export { default as WebhookListItemSkeleton } from "./WebhookListItemSkeleton";
export { default as WebhookListSkeleton } from "./WebhookListSkeleton";
export { default as WebhookTestDisclosure } from "./WebhookTestDisclosure";
@@ -0,0 +1,89 @@
import { useRouter } from "next/router";
import z from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { SkeletonContainer } from "@calcom/ui/v2";
import Meta from "@calcom/ui/v2/core/Meta";
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
import showToast from "@calcom/ui/v2/core/notifications";
import WebhookForm, { WebhookFormSubmitData } from "../components/WebhookForm";
const querySchema = z.object({ id: z.string() });
const EditWebhook = () => {
const { t } = useLocale();
const utils = trpc.useContext();
const router = useRouter();
const { id: webhookId } = querySchema.parse(router.query);
const { data: installedApps, isLoading } = trpc.useQuery(
["viewer.integrations", { variant: "other", onlyInstalled: true }],
{
suspense: true,
enabled: router.isReady,
}
);
const { data: webhook } = trpc.useQuery(["viewer.webhook.get", { webhookId }], {
suspense: true,
enabled: router.isReady,
});
const { data: webhooks } = trpc.useQuery(["viewer.webhook.list"], {
suspense: true,
enabled: router.isReady,
});
const editWebhookMutation = trpc.useMutation("viewer.webhook.edit", {
async onSuccess() {
await utils.invalidateQueries(["viewer.webhook.list"]);
},
onError(error) {
showToast(`${error.message}`, "error");
},
});
const subscriberUrlReserved = (subscriberUrl: string, id: string): boolean => {
return !!webhooks?.find((webhook) => webhook.subscriberUrl === subscriberUrl && webhook.id !== id);
};
if (isLoading || !webhook) return <SkeletonContainer />;
return (
<>
<Meta title="edit_webhook" description="add_webhook_description" backButton />
<WebhookForm
webhook={webhook}
onSubmit={(values: WebhookFormSubmitData) => {
if (subscriberUrlReserved(values.subscriberUrl, webhook.id)) {
showToast(t("webhook_subscriber_url_reserved"), "error");
return;
}
if (values.changeSecret) {
values.secret = values.newSecret.length ? values.newSecret : null;
}
if (!values.payloadTemplate) {
values.payloadTemplate = null;
}
editWebhookMutation.mutate({
id: webhook.id,
subscriberUrl: values.subscriberUrl,
eventTriggers: values.eventTriggers,
active: values.active,
payloadTemplate: values.payloadTemplate,
secret: values.secret,
});
showToast(t("webhook_updated_successfully"), "success");
router.back();
}}
apps={installedApps?.items.map((app) => app.slug)}
/>
</>
);
};
EditWebhook.getLayout = getLayout;
export default EditWebhook;
@@ -0,0 +1,76 @@
import { useRouter } from "next/router";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { SkeletonContainer } from "@calcom/ui/v2";
import Meta from "@calcom/ui/v2/core/Meta";
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
import showToast from "@calcom/ui/v2/core/notifications";
import WebhookForm, { WebhookFormSubmitData } from "../components/WebhookForm";
const NewWebhookView = () => {
const { t } = useLocale();
const utils = trpc.useContext();
const router = useRouter();
// const appId = props.app;
const { data: installedApps, isLoading } = trpc.useQuery(
["viewer.integrations", { variant: "other", onlyInstalled: true }],
{
suspense: true,
enabled: router.isReady,
}
);
const { data: webhooks } = trpc.useQuery(["viewer.webhook.list"], {
suspense: true,
enabled: router.isReady,
});
const createWebhookMutation = trpc.useMutation("viewer.webhook.create", {
async onSuccess() {
await utils.invalidateQueries(["viewer.webhook.list"]);
},
onError(error) {
showToast(`${error.message}`, "error");
},
});
const subscriberUrlReserved = (subscriberUrl: string, id: string): boolean => {
return !!webhooks?.find((webhook) => webhook.subscriberUrl === subscriberUrl && webhook.id !== id);
};
const onCreateWebhook = async (values: WebhookFormSubmitData) => {
if (values.id && subscriberUrlReserved(values.subscriberUrl, values.id)) {
showToast(t("webhook_subscriber_url_reserved"), "error");
return;
}
if (!values.payloadTemplate) {
values.payloadTemplate = null;
}
createWebhookMutation.mutate({
subscriberUrl: values.subscriberUrl,
eventTriggers: values.eventTriggers,
active: values.active,
payloadTemplate: values.payloadTemplate,
secret: values.secret,
});
showToast(t("webhook_created_successfully"), "success");
router.back();
};
if (isLoading) return <SkeletonContainer />;
return (
<>
<Meta title="add_webhook" description="add_webhook_description" backButton />
<WebhookForm onSubmit={onCreateWebhook} apps={installedApps?.items.map((app) => app.slug)} />
</>
);
};
NewWebhookView.getLayout = getLayout;
export default NewWebhookView;
@@ -0,0 +1,81 @@
import { useRouter } from "next/router";
import { Suspense } from "react";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui";
import { ButtonGroup, EmptyScreen, SkeletonText } from "@calcom/ui/v2";
import { Button } from "@calcom/ui/v2/core/Button";
import Meta from "@calcom/ui/v2/core/Meta";
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
import { WebhookListItem, WebhookListSkeleton } from "../components";
const WebhooksView = () => {
return (
<>
<Meta title="webhooks" description="webhooks_description" />
<div>
<Suspense fallback={<WebhookListSkeleton />}>
<WebhooksList />
</Suspense>
</div>
</>
);
};
const NewWebhookButton = () => {
const { t, isLocaleReady } = useLocale();
return (
<Button
color="secondary"
StartIcon={Icon.FiPlus}
href={`${WEBAPP_URL}/v2/settings/developer/webhooks/new`}>
{isLocaleReady ? t("new_webhook") : <SkeletonText className="h-4 w-24" />}
</Button>
);
};
const WebhooksList = () => {
const { t } = useLocale();
const router = useRouter();
const { data: webhooks } = trpc.useQuery(["viewer.webhook.list"], {
suspense: true,
enabled: router.isReady,
});
return (
<>
{webhooks?.length ? (
<>
<div className="mt-6 mb-8 rounded-md border">
{webhooks.map((webhook, index) => (
<WebhookListItem
key={webhook.id}
webhook={webhook}
lastItem={webhooks.length === index + 1}
onEditWebhook={() =>
router.push(`${WEBAPP_URL}/v2/settings/developer/webhooks/${webhook.id} `)
}
/>
))}
</div>
<NewWebhookButton />
</>
) : (
<EmptyScreen
Icon={Icon.FiLink}
headline={t("create_your_first_webhook")}
description={t("create_your_first_webhook_description")}
buttonRaw={<NewWebhookButton />}
/>
)}
</>
);
};
WebhooksView.getLayout = getLayout;
export default WebhooksView;
@@ -1,28 +0,0 @@
const supportedWebhookIntegrationList = ["https://discord.com/api/webhooks/"];
type WebhookIntegrationProps = {
url: string;
};
export const hasTemplateIntegration = (props: WebhookIntegrationProps) => {
const ind = supportedWebhookIntegrationList.findIndex((integration) => {
return props.url.includes(integration);
});
return ind > -1;
};
const customTemplate = (props: WebhookIntegrationProps) => {
const ind = supportedWebhookIntegrationList.findIndex((integration) => {
return props.url.includes(integration);
});
return integrationTemplate(supportedWebhookIntegrationList[ind]) || "";
};
const integrationTemplate = (webhookIntegration: string) => {
switch (webhookIntegration) {
case "https://discord.com/api/webhooks/":
return '{"content": "An event has been scheduled/updated","embeds": [{"color": 2697513,"fields": [{"name": "Event Trigger","value": "{{triggerEvent}}"}, {"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}} "}]}]}';
}
};
export default customTemplate;
@@ -16,10 +16,10 @@ import EventManager from "@calcom/core/EventManager";
import dayjs from "@calcom/dayjs";
import { sendDeclinedEmails, sendLocationChangeEmails, sendScheduledEmails } from "@calcom/emails";
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import getWebhooks from "@calcom/features/webhooks/utils/getWebhooks";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server";
import getSubscribers from "@calcom/lib/webhooks/subscriptions";
import { bookingConfirmPatchBodySchema } from "@calcom/prisma/zod-utils";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
@@ -485,7 +485,7 @@ export const bookingsRouter = createProtectedRouter()
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
};
const subscribersMeetingEnded = await getSubscribers(subscriberOptionsMeetingEnded);
const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded);
subscribersMeetingEnded.forEach((subscriber) => {
updatedBookings.forEach((booking) => {
@@ -92,6 +92,26 @@ export const webhookRouter = createProtectedRouter()
});
},
})
.query("get", {
input: z.object({
webhookId: z.string().optional(),
}),
resolve({ ctx, input }) {
return ctx.prisma.webhook.findUniqueOrThrow({
where: {
id: input.webhookId,
},
select: {
id: true,
subscriberUrl: true,
payloadTemplate: true,
active: true,
eventTriggers: true,
secret: true,
},
});
},
})
.mutation("create", {
input: z.object({
subscriberUrl: z.string().url(),
+4 -2
View File
@@ -6,11 +6,13 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
type MetaType = {
title: string;
description: string;
backButton?: boolean;
};
const initialMeta = {
title: "",
description: "",
backButton: false,
};
const MetaContext = createContext({
@@ -39,12 +41,12 @@ export function MetaProvider({ children }: { children: React.ReactNode }) {
* elsewhere (ie. on a Heading, Title, Subtitle, etc.)
* @example <Meta title="Password" description="Manage settings for your account passwords" />
*/
export default function Meta({ title, description }: MetaType) {
export default function Meta({ title, description, backButton }: MetaType) {
const { t } = useLocale();
const { setMeta, meta } = useMeta();
/* @TODO: maybe find a way to have this data on first render to prevent flicker */
if (meta.title !== title || meta.description !== description) {
setMeta({ title, description });
setMeta({ title, description, backButton });
}
return (
+3 -1
View File
@@ -160,6 +160,7 @@ type InputFieldProps = {
t?: (key: string) => string;
} & React.ComponentProps<typeof Input> & {
labelProps?: React.ComponentProps<typeof Label>;
labelClassName?: string;
};
const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputField(props, ref) {
@@ -170,6 +171,7 @@ const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputF
const {
label = t(name),
labelProps,
labelClassName,
/** Prevents displaying untranslated placeholder keys */
placeholder = t(name + "_placeholder") !== name + "_placeholder" ? t(name + "_placeholder") : "",
className,
@@ -191,7 +193,7 @@ const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputF
<Label
htmlFor={id}
{...labelProps}
className={classNames(labelSrOnly && "sr-only", props.error && "text-red-900")}>
className={classNames(labelClassName, labelSrOnly && "sr-only", props.error && "text-red-900")}>
{label}
</Label>
)}
+4 -1
View File
@@ -2,6 +2,7 @@ import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import React, { ComponentProps, useEffect } from "react";
import ErrorBoundary from "../../../ErrorBoundary";
import Shell from "../Shell";
import SettingsLayout from "./SettingsLayout";
import { UserPermissionRole } from ".prisma/client";
@@ -23,7 +24,9 @@ export default function AdminLayout({
return (
<SettingsLayout {...rest}>
<div className="mx-auto flex max-w-4xl flex-row divide-y divide-gray-200 md:px-12">
<div className="flex flex-1 [&>*]:flex-1">{children}</div>
<div className="flex flex-1 [&>*]:flex-1">
<ErrorBoundary>{children}</ErrorBoundary>
</div>
</div>
</SettingsLayout>
);
+24 -15
View File
@@ -5,6 +5,7 @@ import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/v2/core/Button";
import ErrorBoundary from "../../../ErrorBoundary";
import { Icon } from "../../../Icon";
import { useMeta } from "../Meta";
import Shell from "../Shell";
@@ -52,8 +53,9 @@ const tabs: VerticalTabItemProps[] = [
children: [
//
{ name: "webhooks", href: "/settings/developer/webhooks" },
{ name: "api_keys", href: "/settings/developer/api_keys" },
{ name: "embeds", href: "/settings/developer/embeds" },
{ name: "api_keys", href: "/settings/developer/api-keys" },
// TODO: Add profile level for embeds
// { name: "embeds", href: "/v2/settings/developer/embeds" },
],
},
{
@@ -155,7 +157,7 @@ export default function SettingsLayout({
<div className="flex flex-1 [&>*]:flex-1">
<div className="mx-auto max-w-4xl justify-center">
<ShellHeader />
{children}
<ErrorBoundary>{children}</ErrorBoundary>
</div>
</div>
</Shell>
@@ -169,19 +171,26 @@ function ShellHeader() {
const { t, isLocaleReady } = useLocale();
return (
<header className="mx-auto block max-w-4xl justify-between pt-12 sm:flex sm:pt-8">
<div className="mb-8 w-full border-b border-gray-200 pb-8">
{meta.title && isLocaleReady ? (
<h1 className="font-cal mb-1 text-xl font-bold capitalize tracking-wide text-black">
{t(meta.title)}
</h1>
) : (
<div className="mb-1 h-6 w-24 animate-pulse rounded-md bg-gray-200" />
)}
{meta.description && isLocaleReady ? (
<p className="text-sm text-gray-600 ltr:mr-4 rtl:ml-4">{t(meta.description)}</p>
) : (
<div className="mb-1 h-6 w-32 animate-pulse rounded-md bg-gray-200" />
<div className="mb-8 flex w-full items-center border-b border-gray-200 pb-8">
{meta.backButton && (
<a href="javascript:history.back()">
<Icon.FiArrowLeft className="mr-7" />
</a>
)}
<div>
{meta.title && isLocaleReady ? (
<h1 className="font-cal mb-1 text-xl font-bold capitalize tracking-wide text-black">
{t(meta.title)}
</h1>
) : (
<div className="mb-1 h-6 w-24 animate-pulse rounded-md bg-gray-200" />
)}
{meta.description && isLocaleReady ? (
<p className="text-sm text-gray-600 ltr:mr-4 rtl:ml-4">{t(meta.description)}</p>
) : (
<div className="mb-1 h-6 w-32 animate-pulse rounded-md bg-gray-200" />
)}
</div>
</div>
</header>
);
+1 -1
View File
@@ -18,7 +18,7 @@ const SkeletonText: React.FC<SkeletonBaseProps> = ({ className = "" }) => {
return (
<span
className={classNames(
`font-size-0 dark:white-300 animate-pulse rounded-md bg-gray-300 empty:before:inline-block empty:before:content-['']`,
`font-size-0 dark:white-300 inline-block animate-pulse rounded-md bg-gray-300 empty:before:inline-block empty:before:content-['']`,
className
)}
/>
+1 -1
View File
@@ -23,7 +23,7 @@ export function ListItem(props: ListItemProps) {
{
...passThroughProps,
className: classNames(
"items-center rounded-md bg-white min-w-0 flex-1 flex border-neutral-200 p-2 sm:mx-0 sm:p-8 md:border md:p-4 xl:mt-0",
"items-center rounded-md bg-white min-w-0 flex-1 flex border-neutral-200 p-4 sm:mx-0 md:border md:p-4 xl:mt-0",
expanded ? "my-2 border" : "border -mb-px last:mb-0",
props.className,
(props.onClick || href) && "hover:bg-neutral-50"
@@ -0,0 +1,7 @@
# To all Cal.com contributors
All these components should live in `packages/features`
With love,
zomars 💕