feat: Salesforce Event Type App v3 designs (#21100)

* section component

* more section UI

* wip

* core components styled

* all components using sections

* match icons to figma

* small inputs

* fix select small to be the same as small text

* center flex rows

* enforce inputs to be full spanning grid

* tidy up section container for single inputs

* fix select and i18n

* wip

* install available apps

* raw heading

* app install card

* fix app logos width x height

* fix gap

* match design for other button

* fix prop name

* push i18n string

* fix doc snippets

* fix docs

* chore: remove yarn.lock changes

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
This commit is contained in:
sean-brydonandUdit Takkar authored and GitHub committed 2025-05-06 08:21:20 +00:00
1 parent d0e39cf2dd
commit 073faf9e10
14 files changed
+966 -302

No files matched your search

+1 -1
View File
@@ -24,7 +24,7 @@ export const RenderComponentWithSnippet: React.FC<PropsWithChildren<Props>> = (p
// },
// });
return (
<div className="border-gray-6 bg-gray-1 rounded-lg border">
<div className="border-gray-6 bg-default rounded-lg border">
<div className={className("not-prose p-8 xl:p-12", props.className)}>{props.children}</div>
<div className="bg-gray-3 border-gray-6 flex items-center justify-start border-b border-t p-2">
@@ -0,0 +1,58 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { useState } from "react";
import { Switch } from "@calcom/ui/components/form";
import { Section } from "@calcom/ui/components/section";
export const BasicExample = () => {
const [isOpen, setIsOpen] = useState(true);
return (
<RenderComponentWithSnippet>
<Section>
<Section.Header
title="Basic Section"
description="This is a basic section with a title and description">
<Switch size="sm" checked={isOpen} onCheckedChange={setIsOpen} />
</Section.Header>
{isOpen && (
<Section.Content>
<p>This is the main content of the section.</p>
</Section.Content>
)}
</Section>
</RenderComponentWithSnippet>
);
};
export const BasicExampleSnippet = `
"use client";
import { useState } from "react";
import { Section } from "@calcom/ui/components/section";
import { Switch } from "@calcom/ui/components/form";
const BasicSection = () => {
const [isOpen, setIsOpen] = useState(true);
return (
<Section>
<Section.Header
title="Basic Section"
description="This is a basic section with a title and description"
>
<Section.HeaderRight>
<Switch size="sm" checked={isOpen} onCheckedChange={setIsOpen} />
</Section.HeaderRight>
</Section.Header>
{isOpen && (
<Section.Content>
<p>This is the main content of the section.</p>
</Section.Content>
)}
</Section>
);
};
`;
@@ -0,0 +1,40 @@
---
title: Section
---
import { Section } from "@calcom/ui/components/section"
import { RenderComponentWithSnippet } from "@/app/components/render"
import { Row } from "@/app/components/row"
import { BasicExample } from "./section.basic"
import { WithIconExample } from "./section.withIcon"
import { SubSectionExample } from "./section.subsection"
## Basic Usage
The Section component provides a structured way to organize content with a consistent visual hierarchy. It includes several subcomponents for different parts of the section.
<BasicExample/>
## With Icon
Sections can include an icon in the header to provide visual context.
<WithIconExample/>
## SubSections
Sections can contain subsections for organizing related content in a nested structure.
<SubSectionExample/>
## Components
The Section component is composed of several subcomponents:
- `Section` - The root container
- `Section.Header` - Container for the section header
- `Section.Title` - The section title
- `Section.Description` - Optional description text
- `Section.Content` - The main content area
- `Section.SubSection` - A nested section
- `Section.SubSectionHeader` - Header for a subsection
- `Section.SubSectionContent` - Content area for a subsection
@@ -0,0 +1,151 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { useState } from "react";
import { Input, Switch } from "@calcom/ui/components/form";
import { Section } from "@calcom/ui/components/section";
export const SubSectionExample = () => {
const [isMainOpen, setIsMainOpen] = useState(true);
const [isFirstSubsectionOpen, setIsFirstSubsectionOpen] = useState(true);
const [isSecondSubsectionOpen, setIsSecondSubsectionOpen] = useState(true);
return (
<RenderComponentWithSnippet>
<Section>
<Section.Header
icon="calendar"
title="Section with Subsections"
description="This section contains nested subsections">
<Switch size="sm" checked={isMainOpen} onCheckedChange={setIsMainOpen} />
</Section.Header>
{isMainOpen && (
<Section.Content>
<Section.SubSection>
<Section.SubSectionHeader icon="zap" title="Automatic Sync (Justify Start)" justify="start">
<Switch size="sm" />
</Section.SubSectionHeader>
</Section.SubSection>
<Section.SubSection>
<Section.SubSectionHeader icon="user" title="Create new records for guests added">
<Switch
size="sm"
checked={isFirstSubsectionOpen}
onCheckedChange={setIsFirstSubsectionOpen}
/>
</Section.SubSectionHeader>
{isFirstSubsectionOpen && (
<Section.SubSectionContent>
<p>This is the content of the first subsection.</p>
</Section.SubSectionContent>
)}
</Section.SubSection>
<Section.SubSection>
<Section.SubSectionHeader icon="settings" title="Open Settings">
<Switch
size="sm"
checked={isSecondSubsectionOpen}
onCheckedChange={setIsSecondSubsectionOpen}
/>
</Section.SubSectionHeader>
{isSecondSubsectionOpen && (
<Section.SubSectionContent>
<div className="flex gap-3 px-3 py-1.5">
<div className="text-subtle w-full text-sm font-medium">Field Name</div>
<div className="text-subtle w-full text-sm font-medium">Field Type</div>
<div className="text-subtle w-full text-sm font-medium">Value</div>
<div className="text-subtle w-full text-sm font-medium">When to Write</div>
</div>
<Section.SubSectionNested>
<div className="flex gap-3">
<Input value="Customer Name" className="w-full" />
<Input value="Text" className="w-full" />
<Input value="John Doe" className="w-full" />
<Input value="On Create" className="w-full" />
</div>
</Section.SubSectionNested>
</Section.SubSectionContent>
)}
</Section.SubSection>
</Section.Content>
)}
</Section>
</RenderComponentWithSnippet>
);
};
export const SubSectionExampleSnippet = `
"use client";
import { useState } from "react";
import { Section } from "@calcom/ui/components/section";
import { Switch } from "@calcom/ui/components/form";
const SectionWithSubsections = () => {
const [isMainOpen, setIsMainOpen] = useState(true);
const [isFirstSubsectionOpen, setIsFirstSubsectionOpen] = useState(true);
const [isSecondSubsectionOpen, setIsSecondSubsectionOpen] = useState(true);
return (
<Section>
<Section.Header
icon="calendar"
title="Section with Subsections"
description="This section contains nested subsections">
<Switch size="sm" checked={isMainOpen} onCheckedChange={setIsMainOpen} />
</Section.Header>
{isMainOpen && (
<Section.Content>
<Section.SubSection>
<Section.SubSectionHeader icon="zap" title="Automatic Sync (Justify Start)" justify="start">
<Switch size="sm" />
</Section.SubSectionHeader>
</Section.SubSection>
<Section.SubSection>
<Section.SubSectionHeader icon="user" title="Create new records for guests added">
<Switch
size="sm"
checked={isFirstSubsectionOpen}
onCheckedChange={setIsFirstSubsectionOpen}
/>
</Section.SubSectionHeader>
{isFirstSubsectionOpen && (
<Section.SubSectionContent>
<p>This is the content of the first subsection.</p>
</Section.SubSectionContent>
)}
</Section.SubSection>
<Section.SubSection>
<Section.SubSectionHeader icon="settings" title="Open Settings">
<Switch
size="sm"
checked={isSecondSubsectionOpen}
onCheckedChange={setIsSecondSubsectionOpen}
/>
</Section.SubSectionHeader>
{isSecondSubsectionOpen && (
<Section.SubSectionContent>
<div className="flex gap-3 px-3 py-1.5">
<div className="text-subtle w-full text-sm font-medium">Field Name</div>
<div className="text-subtle w-full text-sm font-medium">Field Type</div>
<div className="text-subtle w-full text-sm font-medium">Value</div>
<div className="text-subtle w-full text-sm font-medium">When to Write</div>
</div>
<Section.SubSectionNested>
<div className="flex gap-3">
<Input value="Customer Name" className="w-full" />
<Input value="Text" className="w-full" />
<Input value="John Doe" className="w-full" />
<Input value="On Create" className="w-full" />
</div>
</Section.SubSectionNested>
</Section.SubSectionContent>
)}
</Section.SubSection>
</Section.Content>
)}
</Section>
);
};
`;
@@ -0,0 +1,63 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { useState } from "react";
import { Switch } from "@calcom/ui/components/form";
import { Section } from "@calcom/ui/components/section";
export const WithIconExample = () => {
const [isOpen, setIsOpen] = useState(true);
return (
<RenderComponentWithSnippet>
<Section>
<Section.Header
icon="calendar"
title="Section with Icon"
description="This section includes an icon in the header. Toggle to see auto-animation.">
<Switch size="sm" checked={isOpen} onCheckedChange={setIsOpen} />
</Section.Header>
{isOpen && (
<Section.Content>
<p>
This section demonstrates how to include an icon in the header and uses FormKit auto-animate for
smooth transitions when toggling content visibility.
</p>
</Section.Content>
)}
</Section>
</RenderComponentWithSnippet>
);
};
export const WithIconExampleSnippet = `
"use client";
import { useState } from "react";
import { Section } from "@calcom/ui/components/section";
import { Switch } from "@calcom/ui/components/form";
const SectionWithIcon = () => {
const [isOpen, setIsOpen] = useState(true);
return (
<Section>
<Section.Header
icon="calendar"
title="Section with Icon"
description="This section includes an icon in the header. Toggle to see auto-animation.">
<Switch size="sm" checked={isOpen} onCheckedChange={setIsOpen} />
</Section.Header>
{isOpen && (
<Section.Content>
<p>
This section demonstrates how to include an icon in the header and uses FormKit auto-animate for
smooth transitions when toggling content visibility.
</p>
</Section.Content>
)}
</Section>
);
};
`;
@@ -2812,6 +2812,7 @@
"salesforce_skip_entry_creation": "Skip creating {{entry}} record if they do not exist in Salesforce",
"salesforce_if_account_does_not_exist": "If the contact does not exist under an account, create new lead from attendee",
"salesforce_create_new_contact_under_account": "Create a new contact under an account based on email domain of attendee and existing contacts",
"salesforce_change_record_owner_on_booking":"Change record owner on booking",
"mass_assign_attributes": "Mass assign attributes",
"reroute_preview_custom_message": "It results in showing custom message. Try changing the response to route to an event",
"reroute_preview_external_redirect": "It results in redirecting to {{externalUrl}}. Try changing the response to route to an event",
@@ -2908,6 +2909,7 @@
"routing_form_insights_assignment_reason": "Assignment Reason",
"access_denied": "Access Denied",
"salesforce_route_to_owner": "Contact owner will be the Round Robin host if available",
"salesforce_add_attendees_as":"Add new attendees as",
"salesforce_do_not_route_to_owner": "Contact owner will not be forced (can still be host if it matches the attributes and Round Robin criteria)",
"salesforce_route_to_custom_lookup_field": "Route to a user that matches a lookup field on an account",
"salesforce_option": "Salesforce Option",
+54 -64
View File
@@ -5,12 +5,11 @@ import { useTranslation } from "react-i18next";
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
import type { RouterOutputs } from "@calcom/trpc/react";
import { Switch } from "@calcom/ui/components/form";
import { Badge } from "@calcom/ui/components/badge";
import { Icon } from "@calcom/ui/components/icon";
import { Avatar } from "@calcom/ui/components/avatar";
import { Button } from "@calcom/ui/components/button";
import classNames from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
import { Switch } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { Section } from "@calcom/ui/components/section";
import type { CredentialOwner } from "../types";
import OmniInstallAppButton from "./OmniInstallAppButton";
@@ -49,45 +48,36 @@ export default function AppCard({
const isPlatform = useIsPlatform();
return (
<div
className={classNames(
"border-subtle",
app?.isInstalled && "mb-4 rounded-md border",
!app.enabled && "grayscale"
)}>
<div className={classNames(app.isInstalled ? "p-4 text-sm sm:p-4" : "px-5 py-4 text-sm sm:px-5")}>
<div className="flex w-full flex-col gap-2 sm:flex-row sm:gap-0">
{/* Don't know why but w-[42px] isn't working, started happening when I started using next/dynamic */}
<Link
href={`/apps/${app.slug}`}
className={classNames(app?.isInstalled ? "mr-[11px]" : "mr-3", "h-auto w-10 rounded-sm")}>
<img
className={classNames(
app?.logo.includes("-dark") && "dark:invert",
app?.isInstalled ? "min-w-[42px]" : "min-w-[40px]",
"w-full"
)}
src={app?.logo}
alt={app?.name}
/>
</Link>
<div className="flex flex-col pe-3">
<div className="text-emphasis">
<span className={classNames(app?.isInstalled && "text-base", "font-semibold leading-4")}>
{app?.name}
</span>
<Section className={classNames(!app?.isInstalled && "rounded-xl")}>
<Section.Header
rawHeading={
<div>
<div className="flex w-full items-center gap-1">
<Section.Title>{app?.name}</Section.Title>
{!app?.isInstalled && (
<span className="bg-emphasis ml-1 rounded px-1 py-0.5 text-xs font-medium leading-3 tracking-[0.01em]">
{app?.categories[0].charAt(0).toUpperCase() + app?.categories[0].slice(1)}
</span>
)}
</div>
<p title={app?.description} className="text-default line-clamp-1 pt-1 text-sm font-normal">
{description || app?.description}
</p>
<Section.Description>{app?.description}</Section.Description>
</div>
<div className="ml-auto flex items-center space-x-2">
{app.credentialOwner && !isPlatform && (
}
iconSlot={
<Link href={`/apps/${app.slug}`} className="flex h-8 w-8 items-center justify-center">
<img
className={classNames(
app?.logo.includes("-dark") && "dark:invert",
"max-h-full max-w-full object-contain"
)}
src={app?.logo}
alt={app?.name}
/>
</Link>
}>
<div>
<div>
{/* {app.credentialOwner && !isPlatform && (
<div className="ml-auto">
<Badge variant="gray">
<div className="flex items-center">
@@ -101,10 +91,11 @@ export default function AppCard({
</div>
</Badge>
</div>
)}
)} */}
{app?.isInstalled || app.credentialOwner ? (
<div className="ml-auto flex items-center">
<Switch
size="sm"
disabled={!app.enabled || managedDisabled || disableSwitch}
onCheckedChange={(enabled) => {
if (switchOnClick) {
@@ -128,32 +119,31 @@ export default function AppCard({
)}
</div>
</div>
</div>
{hideAppCardOptions ? null : (
<div ref={animationRef}>
{app?.isInstalled && switchChecked && <hr className="border-subtle" />}
{app?.isInstalled && switchChecked ? (
app.isSetupAlready === undefined || app.isSetupAlready ? (
<div className="relative p-4 pt-5 text-sm [&_input]:mb-0 [&_input]:leading-4">
{!hideSettingsIcon && !isPlatform && (
<Link href={`/apps/${app.slug}/setup`} className="absolute right-4 top-4">
<Icon name="settings" className="text-default h-4 w-4" aria-hidden="true" />
</Section.Header>
{hideAppCardOptions
? null
: app?.isInstalled &&
switchChecked && (
<div ref={animationRef}>
{app.isSetupAlready === undefined || app.isSetupAlready ? (
<div className="relative text-sm [&_input]:mb-0 [&_input]:leading-4">
{!hideSettingsIcon && !isPlatform && (
<Link href={`/apps/${app.slug}/setup`} className="absolute right-4 top-4">
<Icon name="settings" className="text-default h-4 w-4" aria-hidden="true" />
</Link>
)}
{children}
</div>
) : (
<div className="flex h-64 w-full flex-col items-center justify-center gap-4 ">
<p>{t("this_app_is_not_setup_already")}</p>
<Link href={`/apps/${app.slug}/setup`}>
<Button StartIcon="settings">{t("setup")}</Button>
</Link>
)}
{children}
</div>
) : (
<div className="flex h-64 w-full flex-col items-center justify-center gap-4 ">
<p>{t("this_app_is_not_setup_already")}</p>
<Link href={`/apps/${app.slug}/setup`}>
<Button StartIcon="settings">{t("setup")}</Button>
</Link>
</div>
)
) : null}
</div>
)}
</div>
</div>
)}
</div>
)}
</Section>
);
}
@@ -10,9 +10,10 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import { Alert } from "@calcom/ui/components/alert";
import { Button } from "@calcom/ui/components/button";
import { InputField } from "@calcom/ui/components/form";
import { InputField, Label } from "@calcom/ui/components/form";
import { Select } from "@calcom/ui/components/form";
import { Switch } from "@calcom/ui/components/form";
import { Section } from "@calcom/ui/components/section";
import { showToast } from "@calcom/ui/components/toast";
import { SalesforceRecordEnum } from "../lib/enums";
@@ -74,14 +75,21 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
// Used when creating events under leads or contacts under account
const CreateContactUnderAccount = () => {
return (
<Switch
label={t("salesforce_create_new_contact_under_account")}
labelOnLeading
checked={createNewContactUnderAccount}
onCheckedChange={(checked) => {
setAppData("createNewContactUnderAccount", checked);
}}
/>
<Section.SubSection>
<Section.SubSectionHeader
icon="at-sign"
title={t("salesforce_create_new_contact_under_account")}
labelFor="create-new-contact-under-account">
<Switch
size="sm"
labelOnLeading
checked={createNewContactUnderAccount}
onCheckedChange={(checked) => {
setAppData("createNewContactUnderAccount", checked);
}}
/>
</Section.SubSectionHeader>
</Section.SubSection>
);
};
@@ -95,107 +103,135 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
}}
switchChecked={enabled}
hideSettingsIcon>
<>
<div className="mb-4 ml-2">
<label className="text-emphasis mb-2 align-text-top text-sm font-medium">
{t("salesforce_create_record_as")}
</label>
<Select
className="mt-2 w-60"
options={recordOptions}
value={createEventOnSelectedOption}
onChange={(e) => {
if (e) {
setCreateEventOnSelectedOption(e);
setAppData("createEventOn", e.value);
}
}}
/>
</div>
<div className="mb-4">
<Switch
label={t("salesforce_ignore_guests")}
labelOnLeading
checked={ignoreGuests}
onCheckedChange={(checked) => {
setAppData("ignoreGuests", checked);
}}
/>
</div>
<Section.Content>
<Section.SubSection>
<Section.SubSectionHeader
icon="zap"
title={t("salesforce_add_attendees_as")}
justify="start"
labelFor="add-attendees-as">
<Select
size="sm"
id="add-attendees-as"
className="w-[200px]"
options={recordOptions}
value={createEventOnSelectedOption}
onChange={(e) => {
if (e) {
setCreateEventOnSelectedOption(e);
setAppData("createEventOn", e.value);
}
}}
/>
</Section.SubSectionHeader>
</Section.SubSection>
<Section.SubSection>
<Section.SubSectionHeader
icon="user-plus"
title={t("salesforce_ignore_guests")}
labelFor="ignore-guests">
<Switch
size="sm"
labelOnLeading
checked={ignoreGuests}
onCheckedChange={(checked) => {
setAppData("ignoreGuests", checked);
}}
/>
</Section.SubSectionHeader>
</Section.SubSection>
{createEventOnSelectedOption.value === SalesforceRecordEnum.CONTACT ? (
<div>
<Switch
label={t("skip_contact_creation", { appName: "Salesforce" })}
labelOnLeading
checked={isSkipContactCreationEnabled}
onCheckedChange={(checked) => {
setAppData("skipContactCreation", checked);
}}
/>
</div>
<Section.SubSection>
<Section.SubSectionHeader
icon="user-plus"
title={t("skip_contact_creation", { appName: "Salesforce" })}
labelFor="skip-contact-creation">
<Switch
size="sm"
labelOnLeading
checked={isSkipContactCreationEnabled}
onCheckedChange={(checked) => {
setAppData("skipContactCreation", checked);
}}
/>
</Section.SubSectionHeader>
</Section.SubSection>
) : null}
{createEventOnSelectedOption.value === SalesforceRecordEnum.LEAD ? (
<div>
<Switch
label={t("salesforce_create_event_on_contact")}
labelOnLeading
checked={createEventOnLeadCheckForContact}
onCheckedChange={(checked) => {
setAppData("createEventOnLeadCheckForContact", checked);
}}
/>
<div className="mt-4">
<CreateContactUnderAccount />
</div>
</div>
<>
<Section.SubSection>
<Section.SubSectionHeader
icon="user-plus"
title={t("salesforce_create_event_on_contact")}
labelFor="create-event-on-contact">
<Switch size="sm" labelOnLeading checked={createEventOnLeadCheckForContact} />
</Section.SubSectionHeader>
</Section.SubSection>
<CreateContactUnderAccount />
</>
) : null}
{createEventOnSelectedOption.value === SalesforceRecordEnum.ACCOUNT ? (
<>
<div className="mb-4">
<CreateContactUnderAccount />
</div>
<div>
<Switch
label={t("salesforce_if_account_does_not_exist")}
labelOnLeading
checked={createLeadIfAccountNull}
onCheckedChange={(checked) => {
setAppData("createLeadIfAccountNull", checked);
}}
/>
</div>
<CreateContactUnderAccount />
<Section.SubSection>
<Section.SubSectionHeader
icon="user-plus"
title={t("salesforce_if_account_does_not_exist")}
labelFor="create-lead-if-account-null">
<Switch
size="sm"
labelOnLeading
checked={createLeadIfAccountNull}
onCheckedChange={(checked) => {
setAppData("createLeadIfAccountNull", checked);
}}
/>
</Section.SubSectionHeader>
</Section.SubSection>
</>
) : null}
<div className="mt-4">
<Switch
label={t("on_booking_write_to_event_object")}
labelOnLeading
checked={onBookingWriteToEventObject}
onCheckedChange={(checked) => {
setAppData("onBookingWriteToEventObject", checked);
}}
/>
<Section.SubSection>
<Section.SubSectionHeader icon="badge-check" title={t("on_booking_write_to_event_object")}>
<Switch
size="sm"
labelOnLeading
checked={onBookingWriteToEventObject}
onCheckedChange={(checked) => {
setAppData("onBookingWriteToEventObject", checked);
}}
/>
</Section.SubSectionHeader>
{onBookingWriteToEventObject ? (
<div className="ml-2 mt-2">
<div className="grid grid-cols-3 gap-4">
<div>{t("field_name")}</div>
<div>{t("value")}</div>
<Section.SubSectionContent>
<div className="text-subtle flex gap-3 px-3 py-[6px] text-sm font-medium">
<div className="flex-1">{t("field_name")}</div>
<div className="flex-1">{t("value")}</div>
<div className="w-10" />
</div>
<div>
<Section.SubSectionNested>
{Object.keys(onBookingWriteToEventObjectMap).map((key) => (
<div className="mt-2 grid grid-cols-3 gap-4" key={key}>
<div>
<InputField value={key} readOnly />
<div className="flex items-center gap-2" key={key}>
<div className="flex-1">
<InputField value={key} readOnly size="sm" className="w-full" />
</div>
<div>
<InputField value={onBookingWriteToEventObjectMap[key]} readOnly />
<div className="flex-1">
<InputField
value={onBookingWriteToEventObjectMap[key]}
readOnly
size="sm"
className="w-full"
/>
</div>
<div>
<div className="flex w-10 justify-center">
<Button
StartIcon="trash"
StartIcon="x"
variant="icon"
color="destructive"
size="sm"
color="minimal"
onClick={() => {
const newObject = onBookingWriteToEventObjectMap;
delete onBookingWriteToEventObjectMap[key];
@@ -205,9 +241,11 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
</div>
</div>
))}
<div className="mt-2 grid grid-cols-3 gap-4">
<div>
<div className="mt-2 flex gap-4">
<div className="flex-1">
<InputField
size="sm"
className="w-full"
value={newOnBookingWriteToEventObjectField.field}
onChange={(e) =>
setNewOnBookingWriteToEventObjectField({
@@ -217,8 +255,10 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
}
/>
</div>
<div>
<div className="flex-1">
<InputField
size="sm"
className="w-full"
value={newOnBookingWriteToEventObjectField.value}
onChange={(e) =>
setNewOnBookingWriteToEventObjectField({
@@ -228,11 +268,13 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
}
/>
</div>
<div className="w-10" />
</div>
</div>
</Section.SubSectionNested>
<Button
className="mt-2"
className="text-subtle mt-2 w-fit"
size="sm"
color="secondary"
disabled={
!(newOnBookingWriteToEventObjectField.field && newOnBookingWriteToEventObjectField.value)
}
@@ -255,11 +297,11 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
}}>
{t("add_new_field")}
</Button>
</div>
</Section.SubSectionContent>
) : null}
</div>
</Section.SubSection>
<div className="mt-4">
<Section.SubSection>
<WriteToObjectSettings
bookingAction={BookingActionEnum.ON_BOOKING}
optionLabel={t("salesforce_on_booking_write_to_record", { record: createEventOn })}
@@ -270,89 +312,128 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
}}
updateWriteToObjectData={(data) => setAppData("onBookingWriteToRecordFields", data)}
/>
</div>
</Section.SubSection>
<div className="mt-4">
<Switch
label="Change record owner on booking"
labelOnLeading
checked={onBookingChangeRecordOwner}
onCheckedChange={(checked) => {
setAppData("onBookingChangeRecordOwner", checked);
}}
/>
</div>
{onBookingChangeRecordOwner ? (
<div className="ml-2 mt-2">
<p className="mb-2">{t("salesforce_owner_name_to_change")}</p>
<InputField
value={onBookingChangeRecordOwnerName}
onChange={(e) => setAppData("onBookingChangeRecordOwnerName", e.target.value)}
/>
</div>
) : null}
{eventType.schedulingType === SchedulingType.ROUND_ROBIN ? (
<div className="mt-4">
<Section.SubSection>
<Section.SubSectionHeader
icon="user-plus"
title={t("salesforce_change_record_owner_on_booking")}
labelFor="change-record-owner-on-booking">
<Switch
label={t("salesforce_book_directly_with_attendee_owner")}
size="sm"
labelOnLeading
checked={isRoundRobinLeadSkipEnabled}
checked={onBookingChangeRecordOwner}
onCheckedChange={(checked) => {
setAppData("roundRobinLeadSkip", checked);
if (checked) {
// temporary solution, enabled should always be already set
setAppData("enabled", checked);
}
setAppData("onBookingChangeRecordOwner", checked);
}}
/>
{isRoundRobinLeadSkipEnabled ? (
<>
<div className="my-4 ml-2">
<label className="text-emphasis mb-2 align-text-top text-sm font-medium">
{t("salesforce_check_owner_of")}
</label>
<Select
className="mt-2 w-60"
options={checkOwnerOptions}
value={checkOwnerSelectedOption}
onChange={(e) => {
if (e) {
setCheckOwnerSelectedOption(e);
setAppData("roundRobinSkipCheckRecordOn", e.value);
}
}}
/>
</div>
{checkOwnerSelectedOption.value === SalesforceRecordEnum.CONTACT ? (
<div className="my-4">
<Switch
label={t("salesforce_round_robin_skip_fallback_to_lead_owner")}
labelOnLeading
checked={roundRobinSkipFallbackToLeadOwner}
onCheckedChange={(checked) => {
setAppData("roundRobinSkipFallbackToLeadOwner", checked);
</Section.SubSectionHeader>
{onBookingChangeRecordOwner ? (
<Section.SubSectionContent classNames={{ container: "p-3" }}>
<div>
<Label
htmlFor="on-booking-change-record-owner-name"
className="text-subtle text-sm font-medium">
{t("salesforce_owner_name_to_change")}
</Label>
<InputField
id="on-booking-change-record-owner-name"
size="sm"
value={onBookingChangeRecordOwnerName}
onChange={(e) => setAppData("onBookingChangeRecordOwnerName", e.target.value)}
/>
</div>
</Section.SubSectionContent>
) : null}
</Section.SubSection>
{eventType.schedulingType === SchedulingType.ROUND_ROBIN ? (
<>
<Section.SubSection>
<Section.SubSectionHeader
icon="users"
title={t("salesforce_book_directly_with_attendee_owner")}
labelFor="book-directly-with-attendee-owner">
<Switch
size="sm"
id="book-directly-witha-attendee-owner"
checked={isRoundRobinLeadSkipEnabled}
onCheckedChange={(checked) => {
setAppData("roundRobinLeadSkip", checked);
if (checked) {
// temporary solution, enabled should always be already set
setAppData("enabled", checked);
}
}}
/>
</Section.SubSectionHeader>
{isRoundRobinLeadSkipEnabled ? (
<Section.SubSectionContent classNames={{ container: "p-3" }}>
<div>
<Label
htmlFor="round-robin-skip-check-record-on"
className="text-subtle text-sm font-medium">
{t("salesforce_check_owner_of")}
</Label>
<Select
size="sm"
className="w-60"
options={checkOwnerOptions}
value={checkOwnerSelectedOption}
onChange={(e) => {
if (e) {
setCheckOwnerSelectedOption(e);
setAppData("roundRobinSkipCheckRecordOn", e.value);
}
}}
/>
</div>
) : null}
<div className="my-4">
<Switch
label={t("salesforce_if_free_email_domain_skip_owner_check")}
labelOnLeading
checked={ifFreeEmailDomainSkipOwnerCheck}
onCheckedChange={(checked) => {
setAppData("ifFreeEmailDomainSkipOwnerCheck", checked);
}}
/>
</div>
</>
) : null}
<Alert className="mt-2" severity="neutral" title={t("skip_rr_description")} />
</div>
</Section.SubSectionContent>
) : null}
</Section.SubSection>
<>
{isRoundRobinLeadSkipEnabled ? (
<>
{checkOwnerSelectedOption.value === SalesforceRecordEnum.CONTACT ? (
<Section.SubSection>
<Section.SubSectionHeader
icon="users"
title={t("salesforce_round_robin_skip_fallback_to_lead_owner")}
labelFor="round-robin-skip-fallback-to-lead-owner">
<Switch
id="round-robin-skip-fallback-to-lead-owner"
size="sm"
checked={roundRobinSkipFallbackToLeadOwner}
onCheckedChange={(checked) => {
setAppData("roundRobinSkipFallbackToLeadOwner", checked);
}}
/>
</Section.SubSectionHeader>
</Section.SubSection>
) : null}
<Section.SubSection>
<Section.SubSectionHeader
icon="users"
title={t("salesforce_if_free_email_domain_skip_owner_check")}
labelFor="if-free-email-domain-skip-owner-check">
<Switch
id="if-free-email-domain-skip-owner-check"
size="sm"
checked={ifFreeEmailDomainSkipOwnerCheck}
onCheckedChange={(checked) => {
setAppData("ifFreeEmailDomainSkipOwnerCheck", checked);
}}
/>
</Section.SubSectionHeader>
<Alert severity="info" title={t("skip_rr_description")} />
</Section.SubSection>
</>
) : null}
</>
</>
) : null}
<div className="mt-4">
<Section.SubSection>
<WriteToObjectSettings
bookingAction={BookingActionEnum.ON_CANCEL}
optionLabel={t("salesforce_on_cancel_write_to_event")}
@@ -363,27 +444,39 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
}}
updateWriteToObjectData={(data) => setAppData("onCancelWriteToEventRecordFields", data)}
/>
</div>
</Section.SubSection>
<div className="ml-2 mt-4">
<Switch
label="Send no show attendee data to event object"
checked={sendNoShowAttendeeData}
onCheckedChange={(checked) => {
setAppData("sendNoShowAttendeeData", checked);
}}
/>
<Section.SubSection>
<Section.SubSectionHeader
icon="calendar"
title="Send no show attendee data to event object"
labelFor="send-no-show-attendee-data">
<Switch
id="send-no-show-attendee-data"
size="sm"
checked={sendNoShowAttendeeData}
onCheckedChange={(checked) => {
setAppData("sendNoShowAttendeeData", checked);
}}
/>
</Section.SubSectionHeader>
{sendNoShowAttendeeData ? (
<div className="mt-2">
<p className="mb-2">Field name to check (must be checkbox data type)</p>
<Section.SubSectionContent classNames={{ container: "p-3" }}>
<Label
htmlFor="send-no-show-attendee-data-field-name"
className="text-subtle text-sm font-medium">
Field name to check (must be checkbox data type)
</Label>
<InputField
id="send-no-show-attendee-data-field-name"
size="sm"
value={sendNoShowAttendeeDataField}
onChange={(e) => setAppData("sendNoShowAttendeeDataField", e.target.value)}
/>
</div>
</Section.SubSectionContent>
) : null}
</div>
</>
</Section.SubSection>
</Section.Content>
</AppCard>
);
};
@@ -6,6 +6,7 @@ import { Button } from "@calcom/ui/components/button";
import { Switch } from "@calcom/ui/components/form";
import { InputField } from "@calcom/ui/components/form";
import { Select } from "@calcom/ui/components/form";
import { Section } from "@calcom/ui/components/section";
import { showToast } from "@calcom/ui/components/toast";
import { WhenToWriteToRecord, SalesforceFieldType, DateFieldTypeData } from "../../lib/enums";
@@ -81,37 +82,45 @@ const WriteToObjectSettings = ({
return (
<>
<Switch
label={optionLabel}
labelOnLeading
checked={optionEnabled}
onCheckedChange={optionSwitchOnChange}
/>
<Section.SubSectionHeader icon="star" labelFor="write-to-object-settings" title={optionLabel}>
<Switch
checked={optionEnabled}
onCheckedChange={optionSwitchOnChange}
id="write-to-object-settings"
size="sm"
/>
</Section.SubSectionHeader>
{optionEnabled ? (
<div className="ml-2 mt-2">
<div className="grid grid-cols-5 gap-4">
<div>{t("field_name")}</div>
<div>{t("field_type")}</div>
<div>{t("value")}</div>
<div>{t("when_to_write")}</div>
<Section.SubSectionContent>
<div className="text-subtle flex gap-3 px-3 py-[6px] text-sm font-medium">
<div className="flex-1">{t("field_name")}</div>
<div className="flex-1">{t("field_type")}</div>
<div className="flex-1">{t("value")}</div>
<div className="flex-1">{t("when_to_write")}</div>
<div className="w-10" />
</div>
<div>
<Section.SubSectionNested>
{Object.keys(writeToObjectData).map((key) => (
<div className="mt-2 grid grid-cols-5 gap-4" key={key}>
<div>
<InputField value={key} readOnly />
<div className="flex items-center gap-2" key={key}>
<div className="flex-1">
<InputField value={key} readOnly size="sm" className="w-full" />
</div>
<div>
<div className="flex-1">
<Select
size="sm"
className="w-full"
value={fieldTypeOptions.find(
(option) => option.value === writeToObjectData[key].fieldType
)}
isDisabled={true}
/>
</div>
<div>
<div className="flex-1">
{writeToObjectData[key].fieldType === SalesforceFieldType.DATE ? (
<Select
size="sm"
className="w-full"
value={dateFieldValueOptions.find(
(option) => option.value === writeToObjectData[key].value
)}
@@ -119,28 +128,38 @@ const WriteToObjectSettings = ({
/>
) : writeToObjectData[key].fieldType === SalesforceFieldType.CHECKBOX ? (
<Select
size="sm"
className="w-full"
value={checkboxFieldValueOptions.find(
(option) => option.value === writeToObjectData[key].value
)}
isDisabled={true}
/>
) : (
<InputField value={writeToObjectData[key].value as string} readOnly />
<InputField
value={writeToObjectData[key].value as string}
readOnly
size="sm"
className="w-full"
/>
)}
</div>
<div>
<div className="flex-1">
<Select
size="sm"
className="w-full"
value={whenToWriteToRecordOptions.find(
(option) => option.value === writeToObjectData[key].whenToWrite
)}
isDisabled={true}
/>
</div>
<div>
<div className="flex w-10 justify-center">
<Button
StartIcon="trash"
size="sm"
StartIcon="x"
variant="icon"
color="destructive"
color="minimal"
onClick={() => {
const newObject = writeToObjectData;
delete writeToObjectData[key];
@@ -150,9 +169,11 @@ const WriteToObjectSettings = ({
</div>
</div>
))}
<div className="mt-2 grid grid-cols-5 gap-4">
<div>
<div className="flex gap-2">
<div className="flex-1">
<InputField
size="sm"
className="w-full"
value={newOnWriteToRecordEntry.field}
onChange={(e) =>
setNewOnWriteToRecordEntry({
@@ -162,8 +183,10 @@ const WriteToObjectSettings = ({
}
/>
</div>
<div>
<div className="flex-1">
<Select
size="sm"
className="w-full"
options={fieldTypeOptions}
value={fieldTypeSelectedOption}
onChange={(e) => {
@@ -181,9 +204,11 @@ const WriteToObjectSettings = ({
}}
/>
</div>
<div>
<div className="flex-1">
{newOnWriteToRecordEntry.fieldType === SalesforceFieldType.DATE ? (
<Select
size="sm"
className="w-full"
options={dateFieldValueOptions}
value={dateFieldSelectedOption}
onChange={(e) => {
@@ -198,6 +223,8 @@ const WriteToObjectSettings = ({
/>
) : newOnWriteToRecordEntry.fieldType === SalesforceFieldType.CHECKBOX ? (
<Select
size="sm"
className="w-full"
options={checkboxFieldValueOptions}
value={checkboxFieldSelectedOption}
onChange={(e) => {
@@ -212,6 +239,8 @@ const WriteToObjectSettings = ({
/>
) : (
<InputField
size="sm"
className="w-full"
value={newOnWriteToRecordEntry.value as string}
onChange={(e) =>
setNewOnWriteToRecordEntry({
@@ -222,8 +251,10 @@ const WriteToObjectSettings = ({
/>
)}
</div>
<div>
<div className="flex-1">
<Select
size="sm"
className="w-full"
options={whenToWriteToRecordOptions}
value={whenToWriteSelectedOption}
onChange={(e) => {
@@ -237,10 +268,13 @@ const WriteToObjectSettings = ({
}}
/>
</div>
<div className="w-10" />
</div>
</div>
</Section.SubSectionNested>
<Button
className="mt-2"
className="text-subtle mt-2 w-fit"
StartIcon="plus"
color="minimal"
size="sm"
disabled={
!(
@@ -274,7 +308,7 @@ const WriteToObjectSettings = ({
}}>
{t("add_new_field")}
</Button>
</div>
</Section.SubSectionContent>
) : null}
</>
);
@@ -13,6 +13,7 @@ import { trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/components/alert";
import { Button } from "@calcom/ui/components/button";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { Section } from "@calcom/ui/components/section";
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
EventTypeAppCardComponentProps["eventType"];
@@ -91,7 +92,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
return (
<>
<div>
<div className="before:border-0">
<div className="flex flex-col gap-4 before:border-0">
{(isManagedEventType || isChildrenManagedEventType) && (
<Alert
severity={appsDisableProps.isLocked ? "neutral" : "info"}
@@ -151,14 +152,13 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
})}
</div>
</div>
{/* TODO: Add back after salesforce v3 dev */}
{!appsDisableProps.disabled && (
<div className="bg-muted mt-6 rounded-md p-8">
<div className="bg-muted mt-4 rounded-2xl p-4">
{!isPending && notInstalledApps?.length ? (
<>
<h2 className="text-emphasis mb-2 text-xl font-semibold leading-5 tracking-[0.01em]">
{t("available_apps_lower_case")}
</h2>
<p className="text-default mb-6 text-sm font-normal">
<div className="mb-4 flex flex-col">
<Section.Title>{t("available_apps_lower_case")}</Section.Title>
<Section.Description>
<ServerTrans
t={t}
i18nKey="available_apps_desc"
@@ -168,10 +168,10 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
</Link>,
]}
/>
</p>
</>
</Section.Description>
</div>
) : null}
<div className="bg-default border-subtle divide-subtle divide-y rounded-md border before:border-0">
<div className="bg-default border-muted flex flex-col gap-4 rounded-xl border p-3">
{notInstalledApps?.map((app) => (
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
@@ -52,8 +52,8 @@ export const Select = <
styles={{
control: (base) => ({
...base,
minHeight: size === "sm" ? "28px" : "36px",
height: grow ? "auto" : size === "sm" ? "28px" : "36px",
minHeight: size === "sm" ? "28px" : "32px",
height: grow ? "auto" : size === "sm" ? "28px" : "32px",
}),
}}
classNames={{
@@ -67,7 +67,7 @@ export const Select = <
innerClassNames?.option
),
placeholder: (state) => cx("text-muted", state.isFocused && variant !== "checkbox" && "hidden"),
dropdownIndicator: () => "text-default",
dropdownIndicator: () => cx("text-default w-4 h-4"),
control: (state) =>
cx(
inputStyles({ size }),
@@ -78,8 +78,8 @@ export const Select = <
? "p-1 h-fit"
: "px-3 h-fit"
: size === "sm"
? "h-7 px-2"
: "h-9 px-3",
? "h-7 px-2 py-1"
: "h-8 px-3 py-2",
props.isDisabled && "bg-subtle",
"rounded-[10px]",
"[&:focus-within]:border-emphasis [&:focus-within]:shadow-outline-gray-focused [&:focus-within]:ring-0 !flex",
@@ -108,11 +108,12 @@ export const Select = <
cx(
state.selectProps.menuIsOpen
? hasMultiLastIcons
? "[&>*:last-child]:rotate-180 [&>*:last-child]:transition-transform"
: "rotate-180 transition-transform"
: "text-default" // Woo it adds another SVG here on multi for some reason
? "[&>*:last-child]:rotate-180 [&>*:last-child]:transition-transform [&>*:last-child]:w-4 [&>*:last-child]:h-4"
: "rotate-180 transition-transform w-4 h-4"
: "text-default w-4 h-4"
),
multiValueRemove: () => "text-default py-auto",
...classNames,
}}
{...restProps}
+1
View File
@@ -0,0 +1 @@
export * from "./section";
+230
View File
@@ -0,0 +1,230 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { Slot } from "@radix-ui/react-slot";
import cn from "../../classNames";
import type { IconName } from "../icon";
import { Icon as IconComponent } from "../icon";
const Root = ({
children,
ref,
className,
}: {
children: React.ReactNode;
ref?: React.Ref<HTMLDivElement>;
className?: string;
}) => {
const [animateRef] = useAutoAnimate<HTMLDivElement>();
return (
<div ref={animateRef} className={cn("bg-muted flex flex-col gap-4 rounded-2xl p-4", className)}>
{children}
</div>
);
};
const Title = ({
children,
ref,
as,
}: {
children: React.ReactNode;
ref?: React.Ref<HTMLHeadingElement>;
as?: React.ElementType;
}) => {
const Component = as ? Slot : "h2";
return (
<Component ref={ref} className="text-emphasis text-base font-semibold leading-none">
{children}
</Component>
);
};
const Description = ({
children,
ref,
as,
}: {
children: React.ReactNode;
ref?: React.Ref<HTMLHeadingElement>;
as?: React.ElementType;
}) => {
const Component = as ? Slot : "h3";
return (
<Component ref={ref} className="text-subtle line-clamp-1 text-sm">
{children}
</Component>
);
};
const Icon = ({
ref,
name,
size = "md",
iconSlot,
}: {
ref?: React.Ref<HTMLDivElement>;
size?: "sm" | "md";
name?: IconName;
iconSlot?: React.ReactNode;
} & ({ name: IconName; iconSlot?: never } | { name?: never; iconSlot: React.ReactNode })) => {
return (
<div
ref={ref}
className={cn(
"bg-default border-subtle border-subtle flex items-center justify-center border",
size === "sm" && "rounded-md p-1",
size === "md" && "rounded-[10px] p-1.5"
)}>
{iconSlot ? (
iconSlot
) : (
<IconComponent name={name!} className={cn(size === "sm" && "h-4 w-4", size === "md" && "h-6 w-6")} />
)}
</div>
);
};
const Header = ({
children,
ref,
icon,
title,
description,
iconSlot,
rawHeading,
}: {
children?: React.ReactNode;
ref?: React.Ref<HTMLDivElement>;
icon?: IconName;
iconSlot?: React.ReactNode;
title?: string;
description?: string;
rawHeading?: React.ReactNode;
}) => {
return (
<div ref={ref} className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
{icon && !iconSlot && <Icon name={icon} />}
{iconSlot && <Icon iconSlot={iconSlot} />}
<div className="flex flex-col gap-0.5">
{title && <Title>{title}</Title>}
{description && <Description>{description}</Description>}
{rawHeading && rawHeading}
</div>
</div>
{children}
</div>
);
};
const Content = ({ children, ref }: { children: React.ReactNode; ref?: React.Ref<HTMLDivElement> }) => {
return (
<div ref={ref} className="flex flex-col gap-4">
{children}
</div>
);
};
const SubSection = ({ children, ref }: { children: React.ReactNode; ref?: React.Ref<HTMLDivElement> }) => {
const [animateRef] = useAutoAnimate<HTMLDivElement>();
return (
<div ref={animateRef} className="border-muted bg-default flex flex-col gap-4 rounded-xl border p-3">
{children}
</div>
);
};
const SubSectionHeader = ({
children,
ref,
icon,
title,
classNames,
justify = "between",
labelFor,
}: {
children: React.ReactNode;
icon?: IconName;
title: string;
labelFor?: string;
classNames?: {
container?: string;
title?: string;
description?: string;
};
ref?: React.Ref<HTMLDivElement>;
justify?: "between" | "start";
}) => {
return (
<div
ref={ref}
className={cn(
"flex items-center gap-2",
justify === "between" ? "justify-between" : "justify-start",
classNames?.container
)}>
<div className={cn("flex items-center gap-2", classNames?.title)}>
{icon && <Icon name={icon} size="sm" />}
<div className={cn("flex", classNames?.title)}>
<h4 className="text-default text-sm font-medium leading-none" id={labelFor}>
{title}
</h4>
</div>
</div>
{children}
</div>
);
};
const SubSectionContent = ({
children,
ref,
invert,
classNames,
}: {
children: React.ReactNode;
ref?: React.Ref<HTMLDivElement>;
classNames?: {
container?: string;
};
invert?: boolean;
}) => {
return (
<div
ref={ref}
className={cn(
"bg-muted flex flex-col rounded-lg px-[6px] py-1",
invert && "bg-default border-subtle border",
classNames?.container
)}>
{children}
</div>
);
};
const SubSectionNested = ({
children,
ref,
}: {
children: React.ReactNode;
ref?: React.Ref<HTMLDivElement>;
}) => {
return (
<div ref={ref} className="bg-default border-subtle flex flex-col gap-2 rounded-xl border p-2">
{children}
</div>
);
};
export const Section = Object.assign(Root, {
Header: Header,
Content: Content,
Title: Title,
Description: Description,
SubSection: SubSection,
SubSectionHeader: SubSectionHeader,
SubSectionContent: SubSectionContent,
SubSectionNested: SubSectionNested,
});
+1
View File
@@ -55,6 +55,7 @@
"./components/unpublished-entity": "./components/unpublished-entity/index.ts",
"./components/table/TableNew": "./components/table/TableNew.tsx",
"./components/app-list-card/AppListCard": "./components/app-list-card/AppListCard.tsx",
"./components/section": "./components/section/index.ts",
"./__mocks__/ui": "./__mocks__/ui.tsx"
},
"license": "MIT",