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:
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
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
@@ -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 it’s 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(),
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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 💕
|
||||
Reference in New Issue
Block a user