Merge branch 'main' into integromat-app

This commit is contained in:
aar2dee2 2023-05-17 10:28:33 +05:30
commit 6d9c0fa84e
77 changed files with 880 additions and 384 deletions

43
.gitpod.yml Normal file
View File

@ -0,0 +1,43 @@
tasks:
- init: |
yarn &&
cp .env.example .env &&
next_auth_secret=$(openssl rand -base64 32) &&
calendso_encryption_key=$(openssl rand -base64 24) &&
sed -i -e "s|^NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=$next_auth_secret|" \
-e "s|^CALENDSO_ENCRYPTION_KEY=.*|CALENDSO_ENCRYPTION_KEY=$calendso_encryption_key|" .env
command: yarn dx
ports:
- port: 3000
visibility: public
onOpen: open-preview
- port: 5420
visibility: private
onOpen: ignore
- port: 1025
visibility: private
onOpen: ignore
- port: 8025
visibility: private
onOpen: ignore
github:
prebuilds:
master: true
pullRequests: true
pullRequestsFromForks: true
addCheck: true
addComment: true
addBadge: true
vscode:
extensions:
- DavidAnson.vscode-markdownlint
- yzhang.markdown-all-in-one
- esbenp.prettier-vscode
- dbaeumer.vscode-eslint
- bradlc.vscode-tailwindcss
- ban.spellright
- stripe.vscode-stripe
- Prisma.prisma

View File

@ -158,6 +158,15 @@ yarn dx
```sh
echo 'NEXT_PUBLIC_DEBUG=1' >> .env
```
#### Gitpod Setup
1. Click the button below to open this project in Gitpod.
2. This will open a fully configured workspace in your browser with all the necessary dependencies already installed.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/calcom/cal.com)
#### Manual setup

View File

@ -390,7 +390,12 @@ export const EventSetupTab = (
addOnLeading={
<>
{CAL_URL?.replace(/^(https?:|)\/\//, "")}/
{team ? "team/" + team.slug : eventType.users[0].username}/
{!isManagedEventType
? team
? "team/" + team.slug
: eventType.users[0].username
: t("username_placeholder")}
/
</>
}
{...formMethods.register("slug", {

View File

@ -170,7 +170,7 @@ function EventTypeSingleLayout({
// Define tab navigation here
const EventTypeTabs = useMemo(() => {
let navigation = getNavigation({
const navigation = getNavigation({
t,
eventType,
enabledAppsNumber,
@ -210,7 +210,7 @@ function EventTypeSingleLayout({
}
if (isManagedEventType || isChildrenManagedEventType) {
// Removing apps and workflows for manageg event types by admins v1
navigation = navigation.slice(0, -2);
navigation.splice(-2, 1);
} else {
navigation.push({
name: "webhooks",

View File

@ -31,7 +31,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
{!isBioEmpty ? (
<>
<div
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
/>
</>

View File

@ -26,7 +26,10 @@ const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children
);
// Workaround for https://github.com/vercel/next.js/issues/8592
export type AppProps = Omit<NextAppProps<WithNonceProps & Record<string, unknown>>, "Component"> & {
export type AppProps = Omit<
NextAppProps<WithNonceProps & { themeBasis?: string } & Record<string, unknown>>,
"Component"
> & {
Component: NextAppProps["Component"] & {
requiresLicense?: boolean;
isThemeSupported?: boolean;
@ -72,58 +75,21 @@ const enum ThemeSupport {
Booking = "userConfigured",
}
const CalcomThemeProvider = (
props: PropsWithChildren<
WithNonceProps & {
isBookingPage?: boolean | ((arg: { router: NextRouter }) => boolean);
isThemeSupported?: boolean;
}
>
) => {
// We now support the inverse of how we handled it in the past. Setting this to false will disable theme.
// undefined or true means we use system theme
type CalcomThemeProps = PropsWithChildren<
Pick<AppProps["pageProps"], "nonce" | "themeBasis"> &
Pick<AppProps["Component"], "isBookingPage" | "isThemeSupported">
>;
const CalcomThemeProvider = (props: CalcomThemeProps) => {
const router = useRouter();
const isBookingPage = (() => {
if (typeof props.isBookingPage === "function") {
return props.isBookingPage({ router: router });
}
return props.isBookingPage;
})();
const themeSupport = isBookingPage
? ThemeSupport.Booking
: // if isThemeSupported is explicitly false, we don't use theme there
props.isThemeSupported === false
? ThemeSupport.None
: ThemeSupport.App;
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
// One such example is our Embeds Demo and Testing page at http://localhost:3100
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
const isEmbedMode = typeof embedNamespace === "string";
const storageKey = isEmbedMode
? `embed-theme-${embedNamespace}`
: themeSupport === ThemeSupport.App
? "app-theme"
: themeSupport === ThemeSupport.Booking
? "booking-theme"
: undefined;
return (
<ThemeProvider
nonce={props.nonce}
enableColorScheme={false}
enableSystem={themeSupport !== ThemeSupport.None}
forcedTheme={forcedTheme}
storageKey={storageKey}
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
// This is how login to dashboard soft navigation changes theme from light to dark
key={storageKey}
attribute="class">
<ThemeProvider {...getThemeProviderProps({ props, isEmbedMode, embedNamespace, router })}>
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
{typeof window !== "undefined" && !isEmbedMode && (
@ -140,6 +106,100 @@ const CalcomThemeProvider = (
);
};
/**
* The most important job for this fn is to generate correct storageKey for theme persistenc.
* `storageKey` is important because that key is listened for changes(using [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event) and any pages opened will change it's theme based on that(as part of next-themes implementation).
* Choosing the right storageKey avoids theme flickering caused by another page using different theme
* So, we handle all the cases here namely,
* - Both Booking Pages, /free/30min and /pro/30min but configured with different themes but being operated together.
* - Embeds using different namespace. They can be completely themed different on the same page.
* - Embeds using the same namespace but showing different cal.com links with different themes
* - Embeds using the same namespace and showing same cal.com links with different themes(Different theme is possible for same cal.com link in case of embed because of theme config available in embed)
* - App has different theme then Booking Pages.
*
* All the above cases have one thing in common, which is the origin and thus localStorage is shared and thus `storageKey` is critical to avoid theme flickering.
*
* Some things to note:
* - There is a side effect of so many factors in `storageKey` that many localStorage keys will be created if a user goes through all these scenarios(e.g like booking a lot of different users)
* - Some might recommend disabling localStorage persistence but that doesn't give good UX as then we would default to light theme always for a few seconds before switching to dark theme(if that's the user's preference).
* - We can't disable [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event handling as well because changing theme in one tab won't change the theme without refresh in other tabs. That's again a bad UX
* - Theme flickering becomes infinitely ongoing in case of embeds because of the browser's delay in processing `storage` event within iframes. Consider two embeds simulatenously opened with pages A and B. Note the timeline and keep in mind that it happened
* because 'setItem(A)' and 'Receives storageEvent(A)' allowed executing setItem(B) in b/w because of the delay.
* - t1 -> setItem(A) & Fires storageEvent(A) - On Page A) - Current State(A)
* - t2 -> setItem(B) & Fires storageEvent(B) - On Page B) - Current State(B)
* - t3 -> Receives storageEvent(A) & thus setItem(A) & thus fires storageEvent(A) (On Page B) - Current State(A)
* - t4 -> Receives storageEvent(B) & thus setItem(B) & thus fires storageEvent(B) (On Page A) - Current State(B)
* - ... and so on ...
*/
function getThemeProviderProps({
props,
isEmbedMode,
embedNamespace,
router,
}: {
props: Omit<CalcomThemeProps, "children">;
isEmbedMode: boolean;
embedNamespace: string | null;
router: NextRouter;
}) {
const isBookingPage = (() => {
if (typeof props.isBookingPage === "function") {
return props.isBookingPage({ router: router });
}
return props.isBookingPage;
})();
const themeSupport = isBookingPage
? ThemeSupport.Booking
: // if isThemeSupported is explicitly false, we don't use theme there
props.isThemeSupported === false
? ThemeSupport.None
: ThemeSupport.App;
const isBookingPageThemSupportRequired = themeSupport === ThemeSupport.Booking;
const themeBasis = props.themeBasis;
if ((isBookingPageThemSupportRequired || isEmbedMode) && !themeBasis) {
console.warn(
"`themeBasis` is required for booking page theme support. Not providing it will cause theme flicker."
);
}
const appearanceIdSuffix = themeBasis ? ":" + themeBasis : "";
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
let embedExplicitlySetThemeSuffix = "";
if (typeof window !== "undefined") {
const embedTheme = window.getEmbedTheme();
if (embedTheme) {
embedExplicitlySetThemeSuffix = ":" + embedTheme;
}
}
const storageKey = isEmbedMode
? // Same Namespace, Same Organizer but different themes would still work seamless and not cause theme flicker
// Even though it's recommended to use different namespaces when you want to theme differently on the same page but if the embeds are on different pages, the problem can still arise
`embed-theme-${embedNamespace}${appearanceIdSuffix}${embedExplicitlySetThemeSuffix}`
: themeSupport === ThemeSupport.App
? "app-theme"
: isBookingPageThemSupportRequired
? `booking-theme${appearanceIdSuffix}`
: undefined;
return {
storageKey,
forcedTheme,
themeSupport,
nonce: props.nonce,
enableColorScheme: false,
enableSystem: themeSupport !== ThemeSupport.None,
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
// This is how login to dashboard soft navigation changes theme from light to dark
key: storageKey,
attribute: "class",
};
}
function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
const flags = useFlags();
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
@ -157,6 +217,7 @@ const AppProviders = (props: AppPropsWithChildren) => {
<TooltipProvider>
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
<CalcomThemeProvider
themeBasis={props.pageProps.themeBasis}
nonce={props.pageProps.nonce}
isThemeSupported={props.Component.isThemeSupported}
isBookingPage={props.Component.isBookingPage}>

View File

@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.8.13",
"version": "2.9.0",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@ -115,12 +115,11 @@
"react-select": "^5.7.0",
"react-timezone-select": "^1.4.0",
"react-use-intercom": "1.5.1",
"remark": "^14.0.2",
"remove-markdown": "^0.5.0",
"rrule": "^2.7.1",
"sanitize-html": "^2.10.0",
"schema-dts": "^1.1.0",
"short-uuid": "^4.2.0",
"strip-markdown": "^5.0.0",
"stripe": "^9.16.0",
"superjson": "1.9.1",
"tailwindcss-radix": "^2.6.0",
@ -153,6 +152,7 @@
"@types/qrcode": "^1.4.3",
"@types/react": "18.0.26",
"@types/react-phone-number-input": "^3.0.14",
"@types/remove-markdown": "^0.3.1",
"@types/sanitize-html": "^2.9.0",
"@types/stripe": "^8.0.417",
"@types/uuid": "8.3.1",

View File

@ -23,6 +23,7 @@ import defaultEvents, {
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
import prisma from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@ -37,7 +38,16 @@ import PageWrapper from "@components/PageWrapper";
import { ssrInit } from "@server/lib/ssr";
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
const { users, profile, eventTypes, isDynamicGroup, dynamicNames, dynamicUsernames, isSingleUser } = props;
const {
users,
profile,
eventTypes,
isDynamicGroup,
dynamicNames,
dynamicUsernames,
isSingleUser,
markdownStrippedBio,
} = props;
const [user] = users; //To be used when we only have a single user, not dynamic group
useTheme(user.theme);
const { t } = useLocale();
@ -107,11 +117,9 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<>
<HeadSeo
title={isDynamicGroup ? dynamicNames.join(", ") : nameOrUsername}
description={
isDynamicGroup ? `Book events with ${dynamicUsernames.join(", ")}` : (user.bio as string) || ""
}
description={isDynamicGroup ? `Book events with ${dynamicUsernames.join(", ")}` : markdownStrippedBio}
meeting={{
title: isDynamicGroup ? "" : `${user.bio}`,
title: isDynamicGroup ? "" : markdownStrippedBio,
profile: { name: `${profile.name}`, image: null },
users: isDynamicGroup
? dynamicUsernames.map((username, index) => ({ username, name: dynamicNames[index] }))
@ -136,7 +144,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
{!isBioEmpty && (
<>
<div
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: props.safeBio }}
/>
</>
@ -342,11 +350,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const safeBio = markdownToSafeHTML(user.bio) || "";
const markdownStrippedBio = stripMarkdown(user?.bio || "");
return {
props: {
users,
safeBio,
profile,
// Dynamic group has no theme preference right now. It uses system theme.
themeBasis: isDynamicGroup ? null : user.username,
user: {
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
},
@ -361,6 +372,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
dynamicNames,
dynamicUsernames,
isSingleUser,
markdownStrippedBio,
},
};
};

View File

@ -177,6 +177,8 @@ async function getUserPageProps(context: GetStaticPropsContext) {
slug: `${user.username}/${eventType.slug}`,
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
},
// Dynamic group has no theme preference right now. It uses system theme.
themeBasis: user.username,
away: user?.away,
isDynamic: false,
trpcState: ssg.dehydrate(),
@ -307,6 +309,8 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
props: {
eventType: eventTypeObject,
profile,
// Dynamic group has no theme preference right now. It uses system theme.
themeBasis: null,
isDynamic: true,
away: false,
trpcState: ssg.dehydrate(),

View File

@ -293,6 +293,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
props: {
away: user.away,
profile,
// Dynamic group has no theme preference right now. It uses system theme.
themeBasis: isDynamicGroupBooking ? null : user.username,
eventType: eventTypeObject,
booking,
currentSlotBooking: currentSlotBooking,

View File

@ -46,7 +46,7 @@ class MyDocument extends Document<Props> {
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#000000" />
<meta name="msapplication-TileColor" content="#ff0000" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f9fafb" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1C1C" />
</Head>

View File

@ -4,7 +4,6 @@ import type { SatoriOptions } from "satori";
import { z } from "zod";
import { Meeting, App, Generic } from "@calcom/lib/OgImages";
import { md } from "@calcom/lib/markdownIt";
const calFont = fetch(new URL("../../../../public/fonts/cal.ttf", import.meta.url)).then((res) =>
res.arrayBuffer()
@ -75,12 +74,10 @@ export default async function handler(req: NextApiRequest) {
imageType,
});
const title_ = md.render(title).replace(/(<([^>]+)>)/gi, "");
const img = new ImageResponse(
(
<Meeting
title={title_}
title={title}
profile={{ name: meetingProfileName, image: meetingImage }}
users={names.map((name, index) => ({ name, username: usernames[index] }))}
/>

View File

@ -157,10 +157,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
};
}
let deploymentKey = await getDeploymentKey(prisma);
const deploymentKey = await prisma.deployment.findUnique({
where: { id: 1 },
select: { licenseKey: true },
});
// Check existant CALCOM_LICENSE_KEY env var and acccount for it
if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey) {
if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey?.licenseKey) {
await prisma.deployment.upsert({
where: { id: 1 },
update: {
@ -172,10 +175,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
agreedLicenseAt: new Date(),
},
});
deploymentKey = await getDeploymentKey(prisma);
}
const isFreeLicense = deploymentKey === "";
const isFreeLicense = (await getDeploymentKey(prisma)) === "";
return {
props: {

View File

@ -1092,6 +1092,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
props: {
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
profile,
eventType,

View File

@ -160,6 +160,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return {
props: {
away: user.away,
themeBasis: user.username,
isDynamicGroup: false,
profile,
date,

View File

@ -122,6 +122,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
props: {
profile,
themeBasis: user.username,
eventType: eventTypeObject,
booking: null,
currentSlotBooking: null,

View File

@ -490,18 +490,20 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{t("duplicate")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="outline-none">
<EmbedButton
as={DropdownItem}
type="button"
StartIcon={Code}
className="w-full rounded-none"
embedUrl={encodeURIComponent(embedLink)}>
{t("embed")}
</EmbedButton>
</DropdownMenuItem>
</>
)}
{!isManagedEventType && (
<DropdownMenuItem className="outline-none">
<EmbedButton
as={DropdownItem}
type="button"
StartIcon={Code}
className="w-full rounded-none"
embedUrl={encodeURIComponent(embedLink)}>
{t("embed")}
</EmbedButton>
</DropdownMenuItem>
)}
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) &&
!isChildrenManagedEventType && (

View File

@ -4,7 +4,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
import { CAL_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
@ -12,6 +12,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { Avatar, AvatarGroup, Button, EmptyScreen, HeadSeo } from "@calcom/ui";
@ -26,7 +27,7 @@ import Team from "@components/team/screens/Team";
import { ssrInit } from "@server/lib/ssr";
export type TeamPageProps = inferSSRProps<typeof getServerSideProps>;
function TeamPage({ team, isUnpublished }: TeamPageProps) {
function TeamPage({ team, isUnpublished, markdownStrippedBio }: TeamPageProps) {
useTheme(team.theme);
const showMembers = useToggleQuery("members");
const { t } = useLocale();
@ -67,6 +68,11 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
<div className="px-6 py-4 ">
<Link
href={`/team/${team.slug}/${type.slug}`}
onClick={async () => {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}}
data-testid="event-type-link"
className="flex justify-between">
<div className="flex-shrink">
@ -100,7 +106,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
title={teamName}
description={teamName}
meeting={{
title: team?.bio || "",
title: markdownStrippedBio,
profile: { name: `${team.name}`, image: getPlaceholderAvatar(team.logo, team.name) },
}}
/>
@ -111,7 +117,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
{!isBioEmpty && (
<>
<div
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: team.safeBio }}
/>
</>
@ -196,10 +202,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return { ...member, safeBio: markdownToSafeHTML(member.bio || "") };
});
const markdownStrippedBio = stripMarkdown(team?.bio || "");
return {
props: {
team: { ...team, safeBio, members },
themeBasis: team.slug,
trpcState: ssr.dehydrate(),
markdownStrippedBio,
},
} as const;
};

View File

@ -196,6 +196,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
brandColor: team.brandColor,
darkBrandColor: team.darkBrandColor,
},
themeBasis: team.slug,
date: dateParam,
eventType: eventTypeObject,
workingHours,

View File

@ -153,6 +153,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
image: eventTypeObject.team?.logo || null,
eventName: null,
},
themeBasis: eventTypeObject.team?.slug,
eventType: eventTypeObject,
recurringEventCount,
booking,

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "سيكون الأعضاء قادرين على رؤية التطبيقات النشطة، ولكن من دون القدرة على تعديل أي إعدادات للتطبيق",
"locked_webhooks_description": "سيكون الأعضاء قادرين على رؤية قوالب الويب النشطة، ولكن من دون القدرة على تعديل أي إعدادات للقوالب",
"locked_workflows_description": "سيكون الأعضاء قادرين على رؤية مسارات العمل النشطة، ولكن من دون القدرة على تعديل أي إعدادات لمسار العمل",
"locked_by_admin": "مقفل من قبل المشرف",
"app_not_connected": "أنت غير متصل بحساب {{appName}}.",
"connect_now": "اتصل الآن",
"managed_event_dialog_confirm_button_one": "استبدال وإشعار العضو {{count}}",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "Členové budou vidět aktivní aplikace, ale nastavení aplikací upravovat moci nebudou",
"locked_webhooks_description": "Členové budou vidět aktivní webhooky, ale nastavení webhooků upravovat moci nebudou",
"locked_workflows_description": "Členové budou vidět aktivní pracovní postupy, ale nastavení pracovních postupů upravovat moci nebudou",
"locked_by_admin": "Uzamčeno správcem",
"app_not_connected": "Nemáte připojený účet {{appName}}.",
"connect_now": "Připojte se",
"managed_event_dialog_confirm_button_one": "Nahradit a upozornit {{count}} člena",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "Mitglieder können die aktiven Apps sehen, können aber keine App-Einstellungen anpassen",
"locked_webhooks_description": "Mitglieder können die aktiven Webhooks sehen, können aber keine Webhook-Einstellungen anpassen",
"locked_workflows_description": "Mitglieder können die aktiven Workflows sehen, können aber keine Workflow-Einstellungen anpassen",
"locked_by_admin": "Von Admin gesperrt",
"app_not_connected": "Sie haben kein {{appName}}-Konto verbunden.",
"connect_now": "Jetzt verbinden",
"managed_event_dialog_confirm_button_one": "{{count}} Mitglied ersetzen & benachrichtigen",

View File

@ -1195,6 +1195,7 @@
"create_workflow": "Create a workflow",
"do_this": "Do this",
"turn_off": "Turn off",
"turn_on": "Turn on",
"settings_updated_successfully": "Settings updated successfully",
"error_updating_settings": "Error updating settings",
"personal_cal_url": "My personal {{appName}} URL",
@ -1740,7 +1741,7 @@
"locked_apps_description": "Members will be able to see the active apps but will not be able to edit any app settings",
"locked_webhooks_description": "Members will be able to see the active webhooks but will not be able to edit any webhook settings",
"locked_workflows_description": "Members will be able to see the active workflows but will not be able to edit any workflow settings",
"locked_by_admin": "Locked by admin",
"locked_by_admin": "Locked by team admin",
"app_not_connected": "You have not connected a {{appName}} account.",
"connect_now": "Connect now",
"managed_event_dialog_confirm_button_one": "Replace & notify {{count}} member",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "Los miembros podrán ver las aplicaciones activas pero no podrán editar ninguna configuración de la aplicación",
"locked_webhooks_description": "Los miembros podrán ver los webhooks activos, pero no podrán editar ninguna configuración de webhooks",
"locked_workflows_description": "Los miembros podrán ver los flujos de trabajo activos, pero no podrán editar ninguna configuración de flujo de trabajo",
"locked_by_admin": "Bloqueado por el administrador",
"app_not_connected": "No ha conectado una cuenta de {{appName}}.",
"connect_now": "Conectar ahora",
"managed_event_dialog_confirm_button_one": "Reemplazar y notificar a {{count}} miembro",

View File

@ -1195,6 +1195,7 @@
"create_workflow": "Créer un workflow",
"do_this": "Effectuer ceci",
"turn_off": "Désactiver",
"turn_on": "Activer",
"settings_updated_successfully": "Paramètres mis à jour avec succès",
"error_updating_settings": "Erreur lors de la mise à jour des paramètres",
"personal_cal_url": "Mon lien {{appName}} personnel",
@ -1740,7 +1741,7 @@
"locked_apps_description": "Les membres pourront voir les applications actives, mais ne pourront pas modifier leurs paramètres.",
"locked_webhooks_description": "Les membres pourront voir les webhooks actifs, mais ne pourront pas modifier leurs paramètres.",
"locked_workflows_description": "Les membres pourront voir les workflows actifs, mais ne pourront pas modifier leurs paramètres.",
"locked_by_admin": "Verrouillé par l'administrateur",
"locked_by_admin": "Verrouillé par l'administrateur de l'équipe",
"app_not_connected": "Vous n'avez pas connecté de compte {{appName}}.",
"connect_now": "Connecter maintenant",
"managed_event_dialog_confirm_button_one": "Remplacer et notifier {{count}} membre",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "I membri saranno in grado di vedere le app attive, ma non saranno in grado di modificare le impostazioni delle app",
"locked_webhooks_description": "I membri saranno in grado di vedere i webhook attivi, ma non saranno in grado di modificare le impostazioni dei webhook",
"locked_workflows_description": "I membri saranno in grado di vedere i flussi di lavoro attivi, ma non saranno in grado di modificare le impostazioni dei flussi di lavoro",
"locked_by_admin": "Bloccato dall'amministratore",
"app_not_connected": "Non è stato connesso un account di {{appName}}.",
"connect_now": "Connetti ora",
"managed_event_dialog_confirm_button_one": "Sostituisci e invia notifica a {{count}} membro",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "メンバーは有効なアプリを確認できますが、アプリの設定を編集することはできません",
"locked_webhooks_description": "メンバーは有効なウェブフックを確認できますが、ウェブフックの設定を編集することはできません",
"locked_workflows_description": "メンバーは有効なワークフローを確認できますが、ワークフローの設定を編集することはできません",
"locked_by_admin": "管理者によりロックされています",
"app_not_connected": "{{appName}} のアカウントに接続していません。",
"connect_now": "今すぐ接続",
"managed_event_dialog_confirm_button_one": "{{count}} 人のメンバーを置き換えて通知する",

View File

@ -1737,7 +1737,6 @@
"locked_apps_description": "회원은 활성 앱을 볼 수 있지만 앱 설정을 편집할 수는 없습니다",
"locked_webhooks_description": "회원은 활성 웹훅을 볼 수 있지만 웹훅 설정을 편집할 수는 없습니다",
"locked_workflows_description": "회원은 활성 워크플로를 볼 수 있지만 워크플로 설정을 편집할 수는 없습니다",
"locked_by_admin": "관리자에 의해 잠금 됨",
"app_not_connected": "{{appName}} 계정을 연결하지 않으셨습니다.",
"connect_now": "지금 연결",
"managed_event_dialog_confirm_button_one": "회원 {{count}}명 바꾸기 및 알림",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "Leden kunnen de actieve apps zien, maar kunnen geen appinstellingen bewerken",
"locked_webhooks_description": "Leden kunnen de actieve webhooks zien, maar kunnen geen webhookinstellingen bewerken",
"locked_workflows_description": "Leden kunnen de actieve werkstromen zien, maar kunnen geen werkstroominstellingen bewerken",
"locked_by_admin": "Vergrendeld door beheerder",
"app_not_connected": "U heeft geen {{appName}}-account gekoppeld.",
"connect_now": "Nu koppelen",
"managed_event_dialog_confirm_button_one": "{{count}} lid vervangen en informeren",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "Członkowie będą mogli zobaczyć aktywne aplikacje, ale nie będą mogli zmieniać ich ustawień.",
"locked_webhooks_description": "Członkowie będą mogli zobaczyć aktywne webhooki, ale nie będą mogli zmieniać ich ustawień.",
"locked_workflows_description": "Członkowie będą mogli zobaczyć aktywne przepływy pracy, ale nie będą mogli zmieniać ich ustawień.",
"locked_by_admin": "Zablokowane przez administratora",
"app_not_connected": "Nie połączono konta aplikacji {{appName}}.",
"connect_now": "Połącz teraz",
"managed_event_dialog_confirm_button_one": "Zastąp i poinformuj {{count}} członka",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "Os membros poderão ver os aplicativos ativos, mas não poderão editar as configurações",
"locked_webhooks_description": "Os membros poderão ver os webhooks ativos, mas não poderão editar as configurações",
"locked_workflows_description": "Os membros poderão ver os fluxos de trabalho ativos, mas não poderão editar as configurações",
"locked_by_admin": "Bloqueado pelo administrador",
"app_not_connected": "Você não conectou uma conta do {{appName}}.",
"connect_now": "Conectar agora",
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "Os membros poderão ver as aplicações ativas, mas não poderão editar quaisquer configurações das aplicações",
"locked_webhooks_description": "Os membros poderão ver os webhooks ativos, mas não poderão editar quaisquer configurações dos webhooks",
"locked_workflows_description": "Os membros poderão ver os fluxos de trabalho ativos, mas não poderão editar quaisquer configurações dos fluxos de trabalho",
"locked_by_admin": "Bloqueado pelo administrador",
"app_not_connected": "Não associou uma conta {{appName}}.",
"connect_now": "Associar agora",
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "Membrii vor putea vedea aplicațiile active, însă nu le vor putea modifica deloc setările",
"locked_webhooks_description": "Membrii vor putea vedea webhook-urile active, însă nu le vor putea modifica deloc setările",
"locked_workflows_description": "Membrii vor putea vedea fluxurile de lucru active, însă nu le vor putea modifica deloc setările",
"locked_by_admin": "Blocat de administrator",
"app_not_connected": "Nu ați conectat un cont {{appName}}.",
"connect_now": "Conectați-l acum",
"managed_event_dialog_confirm_button_one": "Înlocuiți și anunțați {{count}} membru",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "Участники смогут видеть активные приложения, но не смогут редактировать их настройки",
"locked_webhooks_description": "Участники смогут видеть активные вебхуки, но не смогут редактировать их настройки",
"locked_workflows_description": "Участники смогут видеть активные рабочие процессы, но не смогут редактировать их настройки",
"locked_by_admin": "Заблокировано администратором",
"app_not_connected": "Вы не подключили аккаунт {{appName}}.",
"connect_now": "Подключить сейчас",
"managed_event_dialog_confirm_button_one": "Заменить и уведомить {{count}} участника",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "Članovi će moći da vide aktivne aplikacije, ali neće moći da uređuju bilo kakva podešavanja aplikacije",
"locked_webhooks_description": "Članovi će moći da vide aktivne webhook-ove, ali neće moći da uređuju bilo kakva podešavanja webhook-ova",
"locked_workflows_description": "Članovi će moći da vide aktivne radne tokove, ali neće moći da uređuju bilo kakva podešavanja radnih tokova",
"locked_by_admin": "Zaključao administrator",
"app_not_connected": "Niste povezali {{appName}} nalog.",
"connect_now": "Povežite odmah",
"managed_event_dialog_confirm_button_one": "Zameni i obavesti {{count}} člana",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "Medlemmarna kommer att kunna se de aktiva apparna, men de kommer inte att kunna ändra appinställningarna",
"locked_webhooks_description": "Medlemmarna kommer att kunna se aktiva webhooks men inte kunna redigera inställningar för webhooks",
"locked_workflows_description": "Medlemmarna kommer att kunna se de aktiva arbetsflödena, men de kommer inte att kunna ändra några arbetsflödesinställningar",
"locked_by_admin": "Låst av administratör",
"app_not_connected": "Du har inte anslutit ett {{appName}}-konto.",
"connect_now": "Anslut nu",
"managed_event_dialog_confirm_button_one": "Ersätt och meddela {{count}} medlem",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "Üyeler aktif uygulamaları görebilir, ancak hiçbir uygulama ayarını düzenleyemez",
"locked_webhooks_description": "Üyeler aktif web kancalarını görebilir, ancak hiçbir web kancası ayarını düzenleyemez",
"locked_workflows_description": "Üyeler aktif iş akışlarını görebilir, ancak hiçbir iş akışı ayarını düzenleyemez",
"locked_by_admin": "Yönetici tarafından kilitlendi",
"app_not_connected": "Bir {{appName}} hesabı bağlamadınız.",
"connect_now": "Hemen bağlan",
"managed_event_dialog_confirm_button_one": "{{count}} üyeyi değiştirin ve bilgilendirin",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "Учасники зможуть бачити активні додатки, але не зможуть редагувати налаштування додатка",
"locked_webhooks_description": "Учасники бачитимуть активні вебгуки, але не зможуть змінювати їх налаштування",
"locked_workflows_description": "Учасники бачитимуть активні робочі процеси, але не зможуть змінювати їх налаштування",
"locked_by_admin": "Заблоковано адміністратором",
"app_not_connected": "Ви не під’єднали обліковий запис {{appName}}.",
"connect_now": "Під’єднати",
"managed_event_dialog_confirm_button_one": "Замінити й повідомити {{count}} учасника",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "Các thành viên có thể thấy được những ứng dụng đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của ứng dụng",
"locked_webhooks_description": "Các thành viên có thể thấy được những webhook đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của webhook",
"locked_workflows_description": "Các thành viên có thể thấy được những tiến độ công việc đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của tiến độ công việc",
"locked_by_admin": "Bị khoá bởi quản trị viên",
"app_not_connected": "Bạn không có tài khoản {{appName}} đã được kết nối.",
"connect_now": "Kết nối ngay",
"managed_event_dialog_confirm_button_one": "Thay thế & thông báo cho {{count}} thành viên",

View File

@ -1191,6 +1191,7 @@
"create_workflow": "创建工作流程",
"do_this": "执行此操作",
"turn_off": "关闭",
"turn_on": "打开",
"settings_updated_successfully": "设置已成功更新",
"error_updating_settings": "更新设置时出错",
"personal_cal_url": "我的个人 {{appName}} URL",
@ -1726,7 +1727,7 @@
"locked_apps_description": "成员将能够查看活动的应用,但不能编辑任何应用设置",
"locked_webhooks_description": "成员将能够查看活动的 Webhook但不能编辑任何 Webhook 设置",
"locked_workflows_description": "成员将能够查看活动的工作流程,但不能编辑任何工作流程设置",
"locked_by_admin": "被管理员锁定",
"locked_by_admin": "被团队管理员锁定",
"app_not_connected": "您尚未连接 {{appName}} 账户。",
"connect_now": "立即连接",
"managed_event_dialog_confirm_button_one": "替换并通知 {{count}} 个成员",

View File

@ -1724,7 +1724,6 @@
"locked_apps_description": "成員將能查看啟用的應用程式,但無法編輯任何應用程式設定",
"locked_webhooks_description": "成員將能查看啟用的 Webhook但無法編輯任何 Webhook 設定",
"locked_workflows_description": "成員將能查看啟用的工作流程,但無法編輯任何工作流程設定",
"locked_by_admin": "已由管理員鎖定",
"app_not_connected": "您尚未連結 {{appName}} 帳號。",
"connect_now": "立即連結",
"managed_event_dialog_confirm_button_one": "取代並通知 {{count}} 位成員",

View File

@ -3,7 +3,7 @@ import type { Prisma } from "@prisma/client";
import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes";
import { buildEventType } from "@calcom/lib/test/builder";
import type { CompleteEventType } from "@calcom/prisma/zod";
import type { CompleteEventType, CompleteWorkflowsOnEventTypes } from "@calcom/prisma/zod";
import { prismaMock } from "../../../../tests/config/singleton";
@ -291,4 +291,85 @@ describe("handleChildrenEventTypes", () => {
expect(result.deletedExistentEventTypes).toEqual([123]);
});
});
describe("Workflows", () => {
it("Links workflows to new and existing assigned members", async () => {
const { schedulingType, id, teamId, locations, timeZone, parentId, userId, ...evType } =
mockFindFirstEventType({
metadata: { managedEventConfig: {} },
locations: [],
workflows: [
{
workflowId: 11,
} as CompleteWorkflowsOnEventTypes,
],
});
prismaMock.$transaction.mockResolvedValue([{ id: 2 }]);
await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [{ userId: 4 }], team: { name: "" } },
children: [
{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: [] } },
{ hidden: false, owner: { id: 5, name: "", email: "", eventTypeSlugs: [] } },
],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
prisma: prismaMock,
});
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
data: {
...evType,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
hashedLink: undefined,
locations: [],
parentId: 1,
userId: 5,
users: {
connect: [
{
id: 5,
},
],
},
workflows: {
create: [{ workflowId: 11 }],
},
},
});
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
data: {
...evType,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
hashedLink: undefined,
workflows: undefined,
scheduleId: undefined,
},
where: {
userId_parentId: {
userId: 4,
parentId: 1,
},
},
});
expect(prismaMock.workflowsOnEventTypes.upsert).toHaveBeenCalledWith({
create: {
eventTypeId: 2,
workflowId: 11,
},
update: {},
where: {
workflowId_eventTypeId: {
eventTypeId: 2,
workflowId: 11,
},
},
});
});
});
});

View File

@ -93,7 +93,7 @@ const DailyVideoApiAdapter = (): VideoApiAdapter => {
const body = await translateEvent(event);
const dailyEvent = await postToDailyAPI(endpoint, body).then(dailyReturnTypeSchema.parse);
const meetingToken = await postToDailyAPI("/meeting-tokens", {
properties: { room_name: dailyEvent.name, is_owner: true },
properties: { room_name: dailyEvent.name, exp: dailyEvent.config.exp, is_owner: true },
}).then(meetingTokenSchema.parse);
return Promise.resolve({

View File

@ -192,6 +192,7 @@ export const getServerSideProps = async function getServerSideProps(
include: {
user: {
select: {
username: true,
theme: true,
brandColor: true,
darkBrandColor: true,
@ -209,6 +210,7 @@ export const getServerSideProps = async function getServerSideProps(
return {
props: {
isEmbed,
themeBasis: form.user.username,
profile: {
theme: form.user.theme,
brandColor: form.user.brandColor,

View File

@ -9,7 +9,7 @@
const randomInt = Math.floor(Math.random() * 16777216);
// Convert the integer to a hex string with 6 digits and add leading zeros if necessary
const hexString = randomInt.toString(16).padStart(6, '0');
const hexString = randomInt.toString(16).padStart(6, "0");
// Return the hex string with a '#' prefix
return `#${hexString}`;
@ -161,7 +161,7 @@
<h3>
<a href="?only=ns:default">[Dark Theme][Guests(janedoe@example.com and test@example.com)]</a>
</h3>
<button onclick="Cal('ui',{theme:'light'})" >Toggle to Light</button>
<button onclick="Cal('ui',{theme:'light'})">Toggle to Light</button>
<i class="last-action"> You would see last Booking page action in my place </i>
<div>
@ -187,7 +187,9 @@
<button onclick="(function () {Cal.ns.second('ui', {styles:{body:{background:'red'}}})})()">
Change <code>body</code> bg color[Deprecated]
</button>
<button onclick="(function () {Cal.ns.second('ui', {styles:{align:'left'}})})()">Align left[Deprecated]</button>
<button onclick="(function () {Cal.ns.second('ui', {styles:{align:'left'}})})()">
Align left[Deprecated]
</button>
<button onclick="(function () {Cal.ns.second('ui', {styles:{align:'center'}})})()">
Align Center[Deprecated]
</button>
@ -234,6 +236,13 @@
<h3><a href="?only=hideEventTypeDetails">Hide EventType Details Test</a></h3>
<div class="place"></div>
</div>
<div class="inline-embed-container" id="cal-booking-place-conflicting-theme">
<h3><a href="?only=conflicting-theme">You would be able to test out conflicting themes for the same namespace here.</a></h3>
<div class="light"></div>
<div class="dark"></div>
<i>Note that one of the embeds would stay in loading state as they are using the same namespace and it is not supported to have more than 1 embeds using same namespace</i>
</div>
</div>
<script src="./playground.ts"></script>
</body>
</html>

View File

@ -245,6 +245,28 @@ if (only === "all" || only === "hideEventTypeDetails") {
);
}
if (only === "conflicting-theme") {
Cal("init", "conflictingTheme", {
debug: true,
origin: "http://localhost:3000",
});
Cal.ns.conflictingTheme("inline", {
elementOrSelector: "#cal-booking-place-conflicting-theme .dark",
calLink: "pro/30min",
config: {
theme: "dark",
},
});
Cal.ns.conflictingTheme("inline", {
elementOrSelector: "#cal-booking-place-conflicting-theme .light",
calLink: "pro/30min",
config: {
theme: "light",
},
});
}
Cal("init", "popupDarkTheme", {
debug: true,
origin: "http://localhost:3000",
@ -268,10 +290,12 @@ Cal("init", "popupAutoTheme", {
debug: true,
origin: "http://localhost:3000",
});
Cal("init", "popupTeamLinkLightTheme", {
debug: true,
origin: "http://localhost:3000",
});
Cal("init", "popupTeamLinkDarkTheme", {
debug: true,
origin: "http://localhost:3000",

View File

@ -70,11 +70,15 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
const timeslot = useBookerStore((state) => state.selectedTimeslot);
const recurringEventCount = useBookerStore((state) => state.recurringEventCount);
const username = useBookerStore((state) => state.username);
const formValues = useBookerStore((state) => state.formValues);
const setFormValues = useBookerStore((state) => state.setFormValues);
const isRescheduling = !!rescheduleUid && !!rescheduleBooking;
const event = useEvent();
const eventType = event.data;
const defaultValues = useMemo(() => {
if (Object.keys(formValues).length) return formValues;
if (!eventType?.bookingFields) {
return {};
}
@ -128,7 +132,7 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
email: defaultUserValues.email,
};
return defaults;
}, [eventType?.bookingFields, isRescheduling, rescheduleBooking, rescheduleUid]);
}, [eventType?.bookingFields, formValues, isRescheduling, rescheduleBooking, rescheduleUid]);
const bookingFormSchema = z
.object({
@ -188,7 +192,6 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
);
},
onError: () => {
errorRef && errorRef.current?.scrollIntoView({ behavior: "smooth" });
},
});
@ -227,6 +230,8 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
);
const bookEvent = (values: BookingFormValues) => {
// Clears form values stored in store, so old values won't stick around.
setFormValues({});
bookingForm.clearErrors();
// It shouldn't be possible that this method is fired without having event data,
@ -281,7 +286,18 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
return (
<div className="flex h-full flex-col">
<Form className="flex h-full flex-col" form={bookingForm} handleSubmit={bookEvent} noValidate>
<Form
className="flex h-full flex-col"
onChange={() => {
// Form data is saved in store. This way when user navigates back to
// still change the timeslot, and comes back to the form, all their values
// still exist. This gets cleared when the form is submitted.
const values = bookingForm.getValues();
setFormValues(values);
}}
form={bookingForm}
handleSubmit={bookEvent}
noValidate>
<BookingFields
isDynamicGroupBooking={!!(username && username.indexOf("+") > -1)}
fields={eventType.bookingFields}
@ -306,7 +322,7 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
/>
</div>
)}
<div className="modalsticky mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
<div className="modalsticky mt-auto flex justify-end space-x-2 rtl:space-x-reverse">
{!!onCancel && (
<Button color="minimal" type="button" onClick={onCancel}>
{t("back")}

View File

@ -79,6 +79,13 @@ type BookerStore = {
* Method called by booker component to set initial data.
*/
initialize: (data: StoreInitializeType) => void;
/**
* Stored form state, used when user navigates back and
* forth between timeslots and form. Get's cleared on submit
* to prevent sticky data.
*/
formValues: Record<string, any>;
setFormValues: (values: Record<string, any>) => void;
};
const validLayouts: BookerLayout[] = ["large_calendar", "large_timeslots", "small_calendar"];
@ -179,6 +186,10 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
set({ selectedTimeslot });
updateQueryParam("slot", selectedTimeslot ?? "");
},
formValues: {},
setFormValues: (formValues: Record<string, any>) => {
set({ formValues });
},
}));
export const useInitializeBookerStore = ({

View File

@ -39,7 +39,7 @@ export const AvailableTimes = ({
return (
<div className={classNames("text-default", className)}>
<header className="bg-muted before:bg-muted mb-5 flex w-full flex-row items-center font-medium md:flex-col md:items-start lg:flex-row lg:items-center">
<header className="bg-muted before:bg-muted mb-5 flex w-full flex-row items-center font-medium">
<span className={classNames(isLargeTimeslots && "w-full text-center")}>
<span className="text-emphasis font-semibold">
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
@ -54,7 +54,7 @@ export const AvailableTimes = ({
</span>
{showTimeformatToggle && (
<div className="ml-auto md:ml-0 lg:ml-auto">
<div className="ml-auto">
<TimeFormatToggle />
</div>
)}

View File

@ -72,7 +72,7 @@ const CalendarSwitch = (props: ICalendarSwitchProps) => {
}
);
return (
<div className={classNames("flex flex-row items-center")}>
<div className={classNames("my-2 flex flex-row items-center")}>
<div className="flex pl-2">
<Switch
id={externalId}

View File

@ -25,6 +25,7 @@ interface handleChildrenEventTypesProps {
oldEventType: {
children?: { userId: number | null }[] | null | undefined;
team: { name: string } | null;
workflows?: { workflowId: number }[];
} | null;
hashedLink: string | undefined;
connectedLink: { id: number } | null;
@ -145,6 +146,9 @@ export default async function handleChildrenEventTypes({
const newUserIds = currentUserIds?.filter((id) => !previousUserIds?.includes(id));
const oldUserIds = currentUserIds?.filter((id) => previousUserIds?.includes(id));
// Calculate if there are new workflows for which assigned members will get too
const currentWorkflowIds = eventType.workflows?.map((wf) => wf.workflowId);
// Define hashedLink query input
const hashedLinkQuery = (userId: number) => {
return hashedLink
@ -190,13 +194,11 @@ export default async function handleChildrenEventTypes({
},
parentId,
hidden: children?.find((ch) => ch.owner.id === userId)?.hidden ?? false,
// Reserved for v2
/*
workflows: eventType.workflows && {
createMany: {
data: eventType.workflows?.map((wf) => ({ ...wf, eventTypeId: undefined })),
},
workflows: currentWorkflowIds && {
create: currentWorkflowIds.map((wfId) => ({ workflowId: wfId })),
},
// Reserved for future releases
/*
webhooks: eventType.webhooks && {
createMany: {
data: eventType.webhooks?.map((wh) => ({ ...wh, eventTypeId: undefined })),
@ -221,7 +223,7 @@ export default async function handleChildrenEventTypes({
});
// Update event types for old users
await prisma.$transaction(
const oldEventTypes = await prisma.$transaction(
oldUserIds.map((userId) => {
return prisma.eventType.update({
where: {
@ -246,21 +248,30 @@ export default async function handleChildrenEventTypes({
})
);
// Reserved for v2
/*const updatedOldWorkflows = await prisma.workflow.updateMany({
where: {
userId: {
in: oldUserIds,
},
},
data: {
...eventType.workflows,
},
});
console.log(
"handleChildrenEventTypes:updatedOldWorkflows",
JSON.stringify({ updatedOldWorkflows }, null, 2)
);
if (currentWorkflowIds?.length) {
await prisma.$transaction(
currentWorkflowIds.flatMap((wfId) => {
return oldEventTypes.map((oEvTy) => {
return prisma.workflowsOnEventTypes.upsert({
create: {
eventTypeId: oEvTy.id,
workflowId: wfId,
},
update: {},
where: {
workflowId_eventTypeId: {
eventTypeId: oEvTy.id,
workflowId: wfId,
},
},
});
});
})
);
}
// Reserved for future releases
/**
const updatedOldWebhooks = await prisma.webhook.updateMany({
where: {
userId: {

View File

@ -264,7 +264,7 @@ const ProfileView = () => {
<>
<Label className="text-emphasis mt-5">{t("about")}</Label>
<div
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600 break-words"
dangerouslySetInnerHTML={{ __html: md.render(team.bio || "") }}
/>
</>

View File

@ -23,6 +23,7 @@ type ItemProps = {
id: number;
title: string;
};
isChildrenManagedEventType: boolean;
};
const WorkflowListItem = (props: ItemProps) => {
@ -131,18 +132,33 @@ const WorkflowListItem = (props: ItemProps) => {
})}
</div>
</div>
<div className="flex-none">
<Link href={`/workflows/${workflow.id}`} passHref={true} target="_blank">
<Button type="button" color="minimal" className="mr-4">
<div className="hidden ltr:mr-2 rtl:ml-2 sm:block">{t("edit")}</div>
<ExternalLink className="text-default -mt-[2px] h-4 w-4 stroke-2" />
</Button>
</Link>
</div>
<Tooltip content={t("turn_off") as string}>
<div className="ltr:mr-2 rtl:ml-2">
{!workflow.readOnly && (
<div className="flex-none">
<Link href={`/workflows/${workflow.id}`} passHref={true} target="_blank">
<Button type="button" color="minimal" className="mr-4">
<div className="hidden ltr:mr-2 rtl:ml-2 sm:block">{t("edit")}</div>
<ExternalLink className="text-default -mt-[2px] h-4 w-4 stroke-2" />
</Button>
</Link>
</div>
)}
<Tooltip
content={
t(
workflow.readOnly && props.isChildrenManagedEventType
? "locked_by_admin"
: isActive
? "turn_off"
: "turn_on"
) as string
}>
<div className="flex items-center ltr:mr-2 rtl:ml-2">
{workflow.readOnly && props.isChildrenManagedEventType && (
<Lock className="text-subtle h-4 w-4 ltr:mr-2 rtl:ml-2" />
)}
<Switch
checked={isActive}
disabled={workflow.readOnly}
onCheckedChange={() => {
activateEventTypeMutation.mutate({ workflowId: workflow.id, eventTypeId: eventType.id });
}}
@ -163,9 +179,14 @@ type Props = {
function EventWorkflowsTab(props: Props) {
const { workflows, eventType } = props;
const { t } = useLocale();
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const { data, isLoading } = trpc.viewer.workflows.list.useQuery({
teamId: eventType.team?.id,
userId: eventType.userId || undefined,
userId: !isChildrenManagedEventType ? eventType.userId || undefined : undefined,
});
const router = useRouter();
const [sortedWorkflows, setSortedWorkflows] = useState<Array<WorkflowType>>([]);
@ -173,7 +194,11 @@ function EventWorkflowsTab(props: Props) {
useEffect(() => {
if (data?.workflows) {
const activeWorkflows = workflows.map((workflowOnEventType) => {
return workflowOnEventType;
const dataWf = data.workflows.find((wf) => wf.id === workflowOnEventType.id);
return {
...workflowOnEventType,
readOnly: isChildrenManagedEventType && dataWf?.teamId ? true : dataWf?.readOnly ?? false,
} as WorkflowType;
});
const disabledWorkflows = data.workflows.filter(
(workflow) =>
@ -204,12 +229,6 @@ function EventWorkflowsTab(props: Props) {
},
});
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
return (
<LicenseRequired>
{!isLoading ? (
@ -217,6 +236,7 @@ function EventWorkflowsTab(props: Props) {
{isManagedEventType && (
<Alert
severity="neutral"
className="mb-2"
title={t("locked_for_members")}
message={t("locked_workflows_description")}
/>
@ -226,7 +246,12 @@ function EventWorkflowsTab(props: Props) {
<div className="space-y-4">
{sortedWorkflows.map((workflow) => {
return (
<WorkflowListItem key={workflow.id} workflow={workflow} eventType={props.eventType} />
<WorkflowListItem
key={workflow.id}
workflow={workflow}
eventType={props.eventType}
isChildrenManagedEventType
/>
);
})}
</div>
@ -238,19 +263,13 @@ function EventWorkflowsTab(props: Props) {
headline={t("workflows")}
description={t("no_workflows_description")}
buttonRaw={
isChildrenManagedEventType && !isManagedEventType ? (
<Button StartIcon={Lock} color="secondary" disabled>
{t("locked_by_admin")}
</Button>
) : (
<Button
target="_blank"
color="secondary"
onClick={() => createMutation.mutate({ teamId: eventType.team?.id })}
loading={createMutation.isLoading}>
{t("create_workflow")}
</Button>
)
<Button
target="_blank"
color="secondary"
onClick={() => createMutation.mutate({ teamId: eventType.team?.id })}
loading={createMutation.isLoading}>
{t("create_workflow")}
</Button>
}
/>
</div>

View File

@ -17,6 +17,7 @@ import type { FormValues } from "../pages/workflow";
type Props = {
form: UseFormReturn<FormValues>;
disabled: boolean;
};
export const TimeTimeUnitInput = (props: Props) => {
@ -33,6 +34,7 @@ export const TimeTimeUnitInput = (props: Props) => {
type="number"
min="1"
label=""
disabled={props.disabled}
defaultValue={form.getValues("time") || 24}
className="-mt-2 rounded-r-none text-sm focus:ring-0"
{...form.register("time", { valueAsNumber: true })}

View File

@ -6,7 +6,7 @@ import { Controller } from "react-hook-form";
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { WorkflowTemplates, SchedulingType } from "@calcom/prisma/enums";
import { WorkflowTemplates } from "@calcom/prisma/enums";
import type { WorkflowActions } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui";
@ -26,6 +26,7 @@ interface Props {
setSelectedEventTypes: Dispatch<SetStateAction<Option[]>>;
teamId?: number;
isMixedEventType: boolean;
readOnly: boolean;
}
export default function WorkflowDetailsPage(props: Props) {
@ -48,15 +49,10 @@ export default function WorkflowDetailsPage(props: Props) {
if (teamId && teamId !== group.teamId) return options;
return [
...options,
...group.eventTypes
.filter(
(evType) =>
!evType.metadata?.managedEventConfig && evType.schedulingType !== SchedulingType.MANAGED
)
.map((eventType) => ({
value: String(eventType.id),
label: eventType.title,
})),
...group.eventTypes.map((eventType) => ({
value: String(eventType.id),
label: `${eventType.title} ${eventType.children.length ? `(+${eventType.children.length})` : ``}`,
})),
];
}, [] as Option[]) || [],
[data]
@ -117,7 +113,12 @@ export default function WorkflowDetailsPage(props: Props) {
<div className="my-8 sm:my-0 md:flex">
<div className="pl-2 pr-3 md:sticky md:top-6 md:h-0 md:pl-0">
<div className="mb-5">
<TextField label={`${t("workflow_name")}:`} type="text" {...form.register("name")} />
<TextField
disabled={props.readOnly}
label={`${t("workflow_name")}:`}
type="text"
{...form.register("name")}
/>
</div>
<Label>{t("which_event_type_apply")}</Label>
<Controller
@ -127,6 +128,7 @@ export default function WorkflowDetailsPage(props: Props) {
return (
<MultiSelectCheckboxes
options={allEventTypeOptions}
isDisabled={props.readOnly}
isLoading={isLoading}
className="w-full md:w-64"
setSelected={setSelectedEventTypes}
@ -139,14 +141,16 @@ export default function WorkflowDetailsPage(props: Props) {
}}
/>
<div className="md:border-subtle my-7 border-transparent md:border-t" />
<Button
type="button"
StartIcon={Trash2}
color="destructive"
className="border"
onClick={() => setDeleteDialogOpen(true)}>
{t("delete_workflow")}
</Button>
{!props.readOnly && (
<Button
type="button"
StartIcon={Trash2}
color="destructive"
className="border"
onClick={() => setDeleteDialogOpen(true)}>
{t("delete_workflow")}
</Button>
)}
<div className="border-subtle my-7 border-t md:border-none" />
</div>
@ -154,7 +158,7 @@ export default function WorkflowDetailsPage(props: Props) {
<div className="bg-muted border-subtle w-full rounded-md border p-3 py-5 md:ml-3 md:p-8">
{form.getValues("trigger") && (
<div>
<WorkflowStepContainer form={form} teamId={teamId} />
<WorkflowStepContainer form={form} teamId={teamId} readOnly={props.readOnly} />
</div>
)}
{form.getValues("steps") && (
@ -168,23 +172,28 @@ export default function WorkflowDetailsPage(props: Props) {
reload={reload}
setReload={setReload}
teamId={teamId}
readOnly={props.readOnly}
/>
);
})}
</>
)}
<div className="my-3 flex justify-center">
<ArrowDown className="text-subtle stroke-[1.5px] text-3xl" />
</div>
<div className="flex justify-center">
<Button
type="button"
onClick={() => setIsAddActionDialogOpen(true)}
color="secondary"
className="bg-default">
{t("add_action")}
</Button>
</div>
{!props.readOnly && (
<>
<div className="my-3 flex justify-center">
<ArrowDown className="text-subtle stroke-[1.5px] text-3xl" />
</div>
<div className="flex justify-center">
<Button
type="button"
onClick={() => setIsAddActionDialogOpen(true)}
color="secondary"
className="bg-default">
{t("add_action")}
</Button>
</div>
</>
)}
</div>
</div>
<AddActionDialog

View File

@ -35,6 +35,10 @@ export type WorkflowType = Workflow & {
eventType: {
id: number;
title: string;
parentId: number | null;
_count: {
children: number;
};
};
}[];
readOnly?: boolean;
@ -112,12 +116,23 @@ export default function WorkflowListPage({ workflows, profileOptions, hasNoWorkf
<Badge variant="gray">
{workflow.activeOn && workflow.activeOn.length > 0 ? (
<Tooltip
content={workflow.activeOn.map((activeOn, key) => (
<p key={key}>{activeOn.eventType.title}</p>
))}>
content={workflow.activeOn
.filter((wf) => (workflow.teamId ? wf.eventType.parentId === null : true))
.map((activeOn, key) => (
<p key={key}>
{activeOn.eventType.title}
{activeOn.eventType._count.children > 0
? ` (+${activeOn.eventType._count.children})`
: ""}
</p>
))}>
<div>
<LinkIcon className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
{t("active_on_event_types", { count: workflow.activeOn.length })}
{t("active_on_event_types", {
count: workflow.activeOn.filter((wf) =>
workflow.teamId ? wf.eventType.parentId === null : true
).length,
})}
</div>
</Tooltip>
) : (

View File

@ -52,6 +52,7 @@ type WorkflowStepProps = {
reload?: boolean;
setReload?: Dispatch<SetStateAction<boolean>>;
teamId?: number;
readOnly: boolean;
};
export default function WorkflowStepContainer(props: WorkflowStepProps) {
@ -249,6 +250,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<Select
isSearchable={false}
className="text-sm"
isDisabled={props.readOnly}
onChange={(val) => {
if (val) {
form.setValue("trigger", val.value);
@ -281,11 +283,13 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
{showTimeSection && (
<div className="mt-5">
<Label>{showTimeSectionAfter ? t("how_long_after") : t("how_long_before")}</Label>
<TimeTimeUnitInput form={form} />
<div className="mt-1 flex text-gray-500">
<Info className="mr-1 mt-0.5 h-4 w-4" />
<p className="text-sm">{t("testing_workflow_info_message")}</p>
</div>
<TimeTimeUnitInput form={form} disabled={props.readOnly} />
{!props.readOnly && (
<div className="mt-1 flex text-gray-500">
<Info className="mr-1 mt-0.5 h-4 w-4" />
<p className="text-sm">{t("testing_workflow_info_message")}</p>
</div>
)}
</div>
)}
</div>
@ -325,39 +329,41 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
</div>
</div>
</div>
<div>
<Dropdown>
<DropdownMenuTrigger asChild>
<Button type="button" color="minimal" variant="icon" StartIcon={MoreHorizontal} />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={Trash2}
color="destructive"
onClick={() => {
const steps = form.getValues("steps");
const updatedSteps = steps
?.filter((currStep) => currStep.id !== step.id)
.map((s) => {
const updatedStep = s;
if (step.stepNumber < updatedStep.stepNumber) {
updatedStep.stepNumber = updatedStep.stepNumber - 1;
}
return updatedStep;
});
form.setValue("steps", updatedSteps);
if (setReload) {
setReload(!reload);
}
}}>
{t("delete")}
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div>
{!props.readOnly && (
<div>
<Dropdown>
<DropdownMenuTrigger asChild>
<Button type="button" color="minimal" variant="icon" StartIcon={MoreHorizontal} />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={Trash2}
color="destructive"
onClick={() => {
const steps = form.getValues("steps");
const updatedSteps = steps
?.filter((currStep) => currStep.id !== step.id)
.map((s) => {
const updatedStep = s;
if (step.stepNumber < updatedStep.stepNumber) {
updatedStep.stepNumber = updatedStep.stepNumber - 1;
}
return updatedStep;
});
form.setValue("steps", updatedSteps);
if (setReload) {
setReload(!reload);
}
}}>
{t("delete")}
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div>
)}
</div>
<div className="border-subtle my-7 border-t" />
<div>
@ -370,6 +376,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<Select
isSearchable={false}
className="text-sm"
isDisabled={props.readOnly}
onChange={(val) => {
if (val) {
const oldValue = form.getValues(`steps.${step.stepNumber - 1}.action`);
@ -468,8 +475,9 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<PhoneInput
placeholder={t("phone_number")}
id={`steps.${step.stepNumber - 1}.sendTo`}
className="min-w-fit sm:rounded-tl-md sm:rounded-bl-md sm:border-r-transparent"
className="min-w-fit sm:rounded-r-none sm:rounded-tl-md sm:rounded-bl-md"
required
disabled={props.readOnly}
value={value}
onChange={(val) => {
const isAlreadyVerified = !!verifiedNumbers
@ -483,9 +491,9 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
/>
<Button
color="secondary"
disabled={numberVerified || false}
disabled={numberVerified || props.readOnly || false}
className={classNames(
"-ml-[3px] h-[40px] min-w-fit sm:block sm:rounded-tl-none sm:rounded-bl-none ",
"-ml-[3px] h-[40px] min-w-fit sm:block sm:rounded-tl-none sm:rounded-bl-none",
numberVerified ? "hidden" : "mt-3 sm:mt-0"
)}
onClick={() =>
@ -508,38 +516,41 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<Badge variant="green">{t("number_verified")}</Badge>
</div>
) : (
<>
<div className="mt-3 flex">
<TextField
className=" border-r-transparent"
placeholder="Verification code"
value={verificationCode}
onChange={(e) => {
setVerificationCode(e.target.value);
}}
required
/>
<Button
color="secondary"
className="-ml-[3px] rounded-tl-none rounded-bl-none "
disabled={verifyPhoneNumberMutation.isLoading}
onClick={() => {
verifyPhoneNumberMutation.mutate({
phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
code: verificationCode,
teamId,
});
}}>
{t("verify")}
</Button>
</div>
{form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
<p className="mt-1 text-xs text-red-500">
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
</p>
)}
</>
!props.readOnly && (
<>
<div className="mt-3 flex">
<TextField
className="rounded-r-none border-r-transparent"
placeholder="Verification code"
disabled={props.readOnly}
value={verificationCode}
onChange={(e) => {
setVerificationCode(e.target.value);
}}
required
/>
<Button
color="secondary"
className="-ml-[3px] h-[38px] min-w-fit sm:block sm:rounded-tl-none sm:rounded-bl-none "
disabled={verifyPhoneNumberMutation.isLoading || props.readOnly}
onClick={() => {
verifyPhoneNumberMutation.mutate({
phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
code: verificationCode,
teamId,
});
}}>
{t("verify")}
</Button>
</div>
{form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
<p className="mt-1 text-xs text-red-500">
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
</p>
)}
</>
)
)}
</div>
)}
@ -551,6 +562,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<Input
type="text"
placeholder={SENDER_ID}
disabled={props.readOnly}
maxLength={11}
{...form.register(`steps.${step.stepNumber - 1}.sender`)}
/>
@ -566,6 +578,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<Label>{t("sender_name")}</Label>
<Input
type="text"
disabled={props.readOnly}
placeholder={SENDER_NAME}
{...form.register(`steps.${step.stepNumber - 1}.senderName`)}
/>
@ -580,6 +593,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
control={form.control}
render={() => (
<Checkbox
disabled={props.readOnly}
defaultChecked={
form.getValues(`steps.${step.stepNumber - 1}.numberRequired`) || false
}
@ -596,6 +610,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<div className="bg-muted mt-5 rounded-md p-4">
<EmailField
required
disabled={props.readOnly}
label={t("email_address")}
{...form.register(`steps.${step.stepNumber - 1}.sendTo`)}
/>
@ -611,6 +626,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<Select
isSearchable={false}
className="text-sm"
isDisabled={props.readOnly}
onChange={(val) => {
if (val) {
if (val.value === WorkflowTemplates.REMINDER) {
@ -657,13 +673,17 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
{isEmailSubjectNeeded && (
<div className="mb-6">
<div className="flex items-center">
<Label className="mb-0 flex-none">{t("subject")}</Label>
<div className="flex-grow text-right">
<AddVariablesDropdown
addVariable={addVariableEmailSubject}
variables={DYNAMIC_TEXT_VARIABLES}
/>
</div>
<Label className={classNames("flex-none", props.readOnly ? "mb-2" : "mb-0")}>
{t("subject")}
</Label>
{!props.readOnly && (
<div className="flex-grow text-right">
<AddVariablesDropdown
addVariable={addVariableEmailSubject}
variables={DYNAMIC_TEXT_VARIABLES}
/>
</div>
)}
</div>
<TextArea
ref={(e) => {
@ -671,6 +691,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
refEmailSubject.current = e;
}}
rows={1}
disabled={props.readOnly}
className="my-0 focus:ring-transparent"
required
{...restEmailSubjectForm}
@ -702,6 +723,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
}}
variables={DYNAMIC_TEXT_VARIABLES}
height="200px"
editable={!props.readOnly}
updateTemplate={updateTemplate}
firstRender={firstRender}
setFirstRender={setFirstRender}
@ -710,15 +732,17 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
) : (
<>
<div className="flex items-center">
<Label className="mb-0 flex-none">
<Label className={classNames("flex-none", props.readOnly ? "mb-2" : "mb-0")}>
{isEmailSubjectNeeded ? t("email_body") : t("text_message")}
</Label>
<div className="flex-grow text-right">
<AddVariablesDropdown
addVariable={addVariableBody}
variables={DYNAMIC_TEXT_VARIABLES}
/>
</div>
{!props.readOnly && (
<div className="flex-grow text-right">
<AddVariablesDropdown
addVariable={addVariableBody}
variables={DYNAMIC_TEXT_VARIABLES}
/>
</div>
)}
</div>
<TextArea
ref={(e) => {
@ -726,6 +750,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
refReminderBody.current = e;
}}
className="my-0 h-24"
disabled={props.readOnly}
required
{...restReminderBodyForm}
/>
@ -737,14 +762,16 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
{form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody?.message || ""}
</p>
)}
<div className="mt-3 ">
<button type="button" onClick={() => setIsAdditionalInputsDialogOpen(true)}>
<div className="text-default mt-2 flex text-sm">
<HelpCircle className="mt-[3px] h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p className="text-left">{t("using_booking_questions_as_variables")}</p>
</div>
</button>
</div>
{!props.readOnly && (
<div className="mt-3 ">
<button type="button" onClick={() => setIsAdditionalInputsDialogOpen(true)}>
<div className="text-default mt-2 flex text-sm">
<HelpCircle className="mt-[3px] h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p className="text-left">{t("using_booking_questions_as_variables")}</p>
</div>
</button>
</div>
)}
</div>
{/* {form.getValues(`steps.${step.stepNumber - 1}.action`) !== WorkflowActions.SMS_ATTENDEE && (

View File

@ -122,10 +122,13 @@ function WorkflowPage() {
setIsMixedEventType(true);
}
setSelectedEventTypes(
workflow.activeOn.map((active) => ({
value: String(active.eventType.id),
label: active.eventType.title,
})) || []
workflow.activeOn.flatMap((active) => {
if (workflow.teamId && active.eventType.parentId) return [];
return {
value: String(active.eventType.id),
label: active.eventType.title,
};
}) || []
);
const activeOn = workflow.activeOn
? workflow.activeOn.map((active) => ({
@ -252,11 +255,11 @@ function WorkflowPage() {
backPath="/workflows"
title={workflow && workflow.name ? workflow.name : "Untitled"}
CTA={
<div>
<Button type="submit" disabled={readOnly}>
{t("save")}
</Button>
</div>
!readOnly && (
<div>
<Button type="submit">{t("save")}</Button>
</div>
)
}
hideHeadingOnMobile
heading={
@ -271,6 +274,11 @@ function WorkflowPage() {
{workflow.team.slug}
</Badge>
)}
{readOnly && (
<Badge className="mt-1 ml-4" variant="gray">
{t("readonly")}
</Badge>
)}
</div>
)
}>
@ -286,6 +294,7 @@ function WorkflowPage() {
setSelectedEventTypes={setSelectedEventTypes}
teamId={workflow ? workflow.teamId || undefined : undefined}
isMixedEventType={isMixedEventType}
readOnly={readOnly}
/>
</>
) : (

View File

@ -66,6 +66,7 @@ export const ChildrenEventTypeSelect = ({
<div className="flex flex-row items-center gap-3 p-3">
<Avatar
size="mdLg"
className="overflow-visible"
imageSrc={`${CAL_URL}/${children.owner.username}/avatar.png`}
alt={children.owner.name || ""}
/>
@ -73,26 +74,20 @@ export const ChildrenEventTypeSelect = ({
<div className="flex flex-col">
<span className="text text-sm font-semibold leading-none">
{children.owner.name}
{children.owner.membership === MembershipRole.OWNER ? (
<Badge className="ml-2" variant="gray">
{t("owner")}
</Badge>
) : (
<Badge className="ml-2" variant="gray">
{t("member")}
</Badge>
)}
<div className="flex flex-row gap-1">
{children.owner.membership === MembershipRole.OWNER ? (
<Badge variant="gray">{t("owner")}</Badge>
) : (
<Badge variant="gray">{t("member")}</Badge>
)}
{children.hidden && <Badge variant="gray">{t("hidden")}</Badge>}
</div>
</span>
<small className="text-subtle font-normal leading-normal">
{`/${children.owner.username}/${children.slug}`}
</small>
</div>
<div className="flex flex-row items-center gap-2">
{children.hidden && (
<Badge variant="gray" className="hidden sm:block">
{t("hidden")}
</Badge>
)}
<Tooltip content={t("show_eventtype_on_profile")}>
<div className="self-center rounded-md p-2">
<Switch

View File

@ -28,7 +28,7 @@ import {
Select,
SkeletonText,
Switch,
Checkbox
Checkbox,
} from "@calcom/ui";
import { Copy, Plus, Trash } from "@calcom/ui/components/icon";
@ -198,7 +198,9 @@ export const DayRanges = <TFieldValues extends FieldValues>({
}}
/>
)}
{index !== 0 && <RemoveTimeButton index={index} remove={remove} className="text-default mx-2 border-none" />}
{index !== 0 && (
<RemoveTimeButton index={index} remove={remove} className="text-default mx-2 border-none" />
)}
</div>
</Fragment>
))}
@ -388,10 +390,10 @@ const CopyTimes = ({
<ol className="space-y-2">
<li key="select all">
<label className="text-default flex w-full items-center justify-between">
<span className="px-1">{t('select_all')}</span>
<span className="px-1">{t("select_all")}</span>
<Checkbox
description={""}
value={t('select_all')}
description=""
value={t("select_all")}
checked={selected.length === 7}
onChange={(e) => {
if (e.target.checked) {
@ -410,7 +412,7 @@ const CopyTimes = ({
<label className="text-default flex w-full items-center justify-between">
<span className="px-1">{weekday}</span>
<Checkbox
description={""}
description=""
value={weekdayIndex}
checked={selected.includes(weekdayIndex) || disabled === weekdayIndex}
disabled={disabled === weekdayIndex}
@ -418,7 +420,7 @@ const CopyTimes = ({
if (e.target.checked && !selected.includes(weekdayIndex)) {
setSelected(selected.concat([weekdayIndex]));
} else if (!e.target.checked && selected.includes(weekdayIndex)) {
setSelected(selected.filter(item => item !== weekdayIndex));
setSelected(selected.filter((item) => item !== weekdayIndex));
}
}}
/>

View File

@ -103,6 +103,11 @@ export default async function getEventTypeById({
successRedirectUrl: true,
currency: true,
bookingFields: true,
parent: {
select: {
teamId: true,
},
},
team: {
select: {
id: true,
@ -191,6 +196,12 @@ export default async function getEventTypeById({
select: {
id: true,
title: true,
parentId: true,
_count: {
select: {
children: true,
},
},
},
},
},

View File

@ -0,0 +1,5 @@
import removeMd from "remove-markdown";
export function stripMarkdown(md: string) {
return removeMd(md);
}

View File

@ -0,0 +1,9 @@
-- Sanitizing the DB before creating unique index
DELETE FROM "WorkflowsOnEventTypes"
WHERE id not in
(SELECT MIN(id)
FROM "WorkflowsOnEventTypes"
GROUP BY "workflowId", "eventTypeId");
-- CreateIndex
CREATE UNIQUE INDEX "WorkflowsOnEventTypes_workflowId_eventTypeId_key" ON "WorkflowsOnEventTypes"("workflowId", "eventTypeId");

View File

@ -713,6 +713,7 @@ model WorkflowsOnEventTypes {
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
eventTypeId Int
@@unique([workflowId, eventTypeId])
@@index([workflowId])
@@index([eventTypeId])
}

View File

@ -46,6 +46,7 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
users: true,
},
},
parentId: true,
hosts: {
select: {
user: {

View File

@ -248,6 +248,11 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
userId: true,
},
},
workflows: {
select: {
workflowId: true,
},
},
team: {
select: {
name: true,

View File

@ -48,6 +48,9 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
},
],
},
include: {
children: true,
},
});
if (!userEventType)
@ -118,13 +121,15 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
await prisma.workflowsOnEventTypes.deleteMany({
where: {
workflowId,
eventTypeId,
eventTypeId: { in: [eventTypeId].concat(userEventType.children.map((ch) => ch.id)) },
},
});
await removeSmsReminderFieldForBooking({
workflowId,
eventTypeId,
[eventTypeId].concat(userEventType.children.map((ch) => ch.id)).map(async (chId) => {
await removeSmsReminderFieldForBooking({
workflowId,
eventTypeId: chId,
});
});
} else {
// activate workflow and schedule reminders for existing bookings
@ -220,11 +225,13 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
}
}
await prisma.workflowsOnEventTypes.create({
data: {
workflowId,
eventTypeId,
},
await prisma.workflowsOnEventTypes.createMany({
data: [
{
workflowId,
eventTypeId,
},
].concat(userEventType.children.map((ch) => ({ workflowId, eventTypeId: ch.id }))),
});
if (
@ -235,10 +242,12 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
const isSmsReminderNumberRequired = eventTypeWorkflow.steps.some((step) => {
return step.action === WorkflowActions.SMS_ATTENDEE && step.numberRequired;
});
await upsertSmsReminderFieldForBooking({
workflowId,
isSmsReminderNumberRequired,
eventTypeId,
[eventTypeId].concat(userEventType.children.map((ch) => ch.id)).map(async (evTyId) => {
await upsertSmsReminderFieldForBooking({
workflowId,
isSmsReminderNumberRequired,
eventTypeId: evTyId,
});
});
}
}

View File

@ -43,6 +43,12 @@ export const listHandler = async ({ ctx, input }: ListOptions) => {
select: {
id: true,
title: true,
parentId: true,
_count: {
select: {
children: true,
},
},
},
},
},
@ -75,6 +81,12 @@ export const listHandler = async ({ ctx, input }: ListOptions) => {
select: {
id: true,
title: true,
parentId: true,
_count: {
select: {
children: true,
},
},
},
},
},
@ -120,6 +132,12 @@ export const listHandler = async ({ ctx, input }: ListOptions) => {
select: {
id: true,
title: true,
parentId: true,
_count: {
select: {
children: true,
},
},
},
},
},

View File

@ -64,15 +64,46 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const activeOnEventTypes = await ctx.prisma.eventType.findMany({
where: {
id: {
in: activeOn,
},
},
select: {
id: true,
children: {
select: {
id: true,
},
},
},
});
const activeOnWithChildren = activeOnEventTypes
.map((eventType) => [eventType.id].concat(eventType.children.map((child) => child.id)))
.flat();
const oldActiveOnEventTypes = await ctx.prisma.workflowsOnEventTypes.findMany({
where: {
workflowId: id,
},
select: {
eventTypeId: true,
eventType: {
include: {
children: true,
},
},
},
});
const oldActiveOnEventTypeIds = oldActiveOnEventTypes
.map((eventTypeRel) =>
[eventTypeRel.eventType.id].concat(eventTypeRel.eventType.children.map((child) => child.id))
)
.flat();
const newActiveEventTypes = activeOn.filter((eventType) => {
if (
!oldActiveOnEventTypes ||
@ -99,6 +130,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
members: true,
},
},
children: true,
},
});
@ -119,15 +151,11 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
}
//remove all scheduled Email and SMS reminders for eventTypes that are not active any more
const removedEventTypes = oldActiveOnEventTypes
.map((eventType) => {
return eventType.eventTypeId;
})
.filter((eventType) => {
if (!activeOn.includes(eventType)) {
return eventType;
}
});
const removedEventTypes = oldActiveOnEventTypeIds.filter((eventTypeId) => {
if (!activeOnWithChildren.includes(eventTypeId)) {
return eventTypeId;
}
});
const remindersToDeletePromise: Prisma.PrismaPromise<
{
@ -189,7 +217,16 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
//create reminders for all bookings with newEventTypes
const bookingsForReminders = await ctx.prisma.booking.findMany({
where: {
eventTypeId: { in: newEventTypes },
OR: [
{ eventTypeId: { in: newEventTypes } },
{
eventType: {
parentId: {
in: newEventTypes,
},
},
},
],
status: BookingStatus.ACCEPTED,
startTime: {
gte: new Date(),
@ -288,13 +325,24 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
});
}
//create all workflow - eventtypes relationships
activeOn.forEach(async (eventTypeId) => {
activeOnEventTypes.forEach(async (eventType) => {
await ctx.prisma.workflowsOnEventTypes.createMany({
data: {
workflowId: id,
eventTypeId,
eventTypeId: eventType.id,
},
});
if (eventType.children.length) {
eventType.children.forEach(async (chEventType) => {
await ctx.prisma.workflowsOnEventTypes.createMany({
data: {
workflowId: id,
eventTypeId: chEventType.id,
},
});
});
}
});
}
@ -666,7 +714,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
});
}
for (const eventTypeId of activeOn) {
for (const eventTypeId of activeOnWithChildren) {
if (smsReminderNumberNeeded) {
await upsertSmsReminderFieldForBooking({
workflowId: id,

View File

@ -80,10 +80,16 @@ export const Editor = (props: TextEditorProps) => {
setFirstRender={props.setFirstRender}
/>
<div
className={classNames("editor-inner scroll-bar", !editable && "bg-muted")}
className={classNames("editor-inner scroll-bar", !editable && "!bg-subtle")}
style={{ height: props.height }}>
<RichTextPlugin
contentEditable={<ContentEditable style={{ height: props.height }} className="editor-input" />}
contentEditable={
<ContentEditable
readOnly={!editable}
style={{ height: props.height }}
className="editor-input"
/>
}
placeholder={<div className="text-muted -mt-11 p-3 text-sm">{props.placeholder || ""}</div>}
ErrorBoundary={LexicalErrorBoundary}
/>

View File

@ -55,7 +55,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
className={classNames(
"text-primary-600 focus:ring-primary-500 border-default bg-default h-4 w-4 rounded ltr:mr-2 rtl:ml-2",
!error && disabled
? "bg-gray-300 checked:bg-gray-300"
? "cursor-not-allowed bg-gray-300 checked:bg-gray-300 hover:bg-gray-300 hover:checked:bg-gray-300"
: "hover:bg-subtle checked:bg-gray-800",
error && "border-red-800 checked:bg-red-800 hover:bg-red-400",
rest.className

View File

@ -65,6 +65,7 @@ export default function MultiSelectCheckboxes({
setSelected,
setValue,
className,
isDisabled,
}: Omit<Props, "options"> & MultiSelectionCheckboxesProps) {
const additonalComponents = { MultiValue };
@ -78,6 +79,7 @@ export default function MultiSelectCheckboxes({
variant="checkbox"
options={options}
isMulti
isDisabled={isDisabled}
className={classNames(className ? className : "w-64 text-sm")}
isSearchable={false}
closeMenuOnSelect={false}

View File

@ -74,7 +74,7 @@ const Addon = ({ isFilled, children, className, error }: AddonProps) => (
)}>
<div
className={classNames(
"flex h-9 flex-col justify-center text-sm",
"min-h-9 flex flex-col justify-center text-sm leading-7",
error ? "text-error" : "text-default"
)}>
<span className="flex whitespace-nowrap">{children}</span>
@ -143,7 +143,7 @@ export const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function
isStandaloneField={false}
className={classNames(
className,
"disabled:bg-muted disabled:hover:border-subtle disabled:cursor-not-allowed",
"disabled:bg-subtle disabled:hover:border-subtle disabled:cursor-not-allowed",
addOnLeading && "rounded-l-none border-l-0",
addOnSuffix && "rounded-r-none border-r-0",
type === "search" && "pr-8",
@ -184,7 +184,7 @@ export const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function
placeholder={placeholder}
className={classNames(
className,
"disabled:bg-muted disabled:hover:border-subtle disabled:cursor-not-allowed"
"disabled:bg-subtle disabled:hover:border-subtle disabled:cursor-not-allowed"
)}
{...passThrough}
readOnly={readOnly}
@ -280,7 +280,7 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function
ref={ref}
{...props}
className={classNames(
"hover:border-emphasis border-default bg-default placeholder:text-muted text-emphasis disabled:hover:border-default mb-2 block w-full rounded-md border py-2 px-3 text-sm leading-4 focus:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-800 focus:ring-offset-1 disabled:cursor-not-allowed",
"hover:border-emphasis border-default bg-default placeholder:text-muted text-emphasis disabled:hover:border-default disabled:bg-subtle mb-2 block w-full rounded-md border py-2 px-3 text-sm focus:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-800 focus:ring-offset-1 disabled:cursor-not-allowed",
props.className
)}
/>

View File

@ -59,7 +59,7 @@ export const Select = <
? "p-1"
: "px-3 py-2"
: "py-2 px-3",
props.isDisabled && "bg-muted",
props.isDisabled && "bg-subtle",
props.classNames?.control
),
singleValue: () => cx("text-emphasis placeholder:text-muted", props.classNames?.singleValue),

View File

@ -1,6 +1,6 @@
import * as RadixToggleGroup from "@radix-ui/react-toggle-group";
import type { ReactNode } from "react";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { classNames } from "@calcom/lib";
import { Tooltip } from "@calcom/ui";
@ -29,7 +29,7 @@ const OptionalTooltipWrapper = ({
export const ToggleGroup = ({ options, onValueChange, isFullWidth, ...props }: ToggleGroupProps) => {
const [value, setValue] = useState<string | undefined>(props.defaultValue);
const [activeToggleElement, setActiveToggleElement] = useState<null | HTMLButtonElement>(null);
const activeRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
if (value && onValueChange) onValueChange(value);
@ -48,9 +48,15 @@ export const ToggleGroup = ({ options, onValueChange, isFullWidth, ...props }: T
)}>
{/* Active toggle. It's a separate element so we can animate it nicely. */}
<span
ref={activeRef}
aria-hidden
className="bg-emphasis absolute top-[4px] bottom-[4px] left-0 z-[0] rounded-[4px] transition-all"
style={{ left: activeToggleElement?.offsetLeft, width: activeToggleElement?.offsetWidth }}
className={classNames(
"bg-emphasis absolute top-[4px] bottom-[4px] left-0 z-[0] rounded-[4px]",
// Disable the animation until after initial render, that way when the component would
// rerender the styles are immediately set and we don't see a flash moving the element
// into position because of the animation.
activeRef?.current && "transition-all"
)}
/>
{options.map((option) => (
<OptionalTooltipWrapper key={option.value} tooltipText={option.tooltip}>
@ -65,8 +71,12 @@ export const ToggleGroup = ({ options, onValueChange, isFullWidth, ...props }: T
isFullWidth && "w-full"
)}
ref={(node) => {
if (node && value === option.value && activeToggleElement !== node) {
setActiveToggleElement(node);
if (node && value === option.value) {
// Sets position of active toggle element with inline styles.
// This way we trigger as little rerenders as possible.
if (!activeRef.current || activeRef?.current.style.left === `${node.offsetLeft}px`) return;
activeRef.current.style.left = `${node.offsetLeft}px`;
activeRef.current.style.width = `${node.offsetWidth}px`;
}
return node;
}}>

View File

@ -4957,6 +4957,7 @@ __metadata:
"@types/qrcode": ^1.4.3
"@types/react": 18.0.26
"@types/react-phone-number-input": ^3.0.14
"@types/remove-markdown": ^0.3.1
"@types/sanitize-html": ^2.9.0
"@types/stripe": ^8.0.417
"@types/turndown": ^5.0.1
@ -5023,12 +5024,11 @@ __metadata:
react-select: ^5.7.0
react-timezone-select: ^1.4.0
react-use-intercom: 1.5.1
remark: ^14.0.2
remove-markdown: ^0.5.0
rrule: ^2.7.1
sanitize-html: ^2.10.0
schema-dts: ^1.1.0
short-uuid: ^4.2.0
strip-markdown: ^5.0.0
stripe: ^9.16.0
superjson: 1.9.1
tailwindcss: ^3.3.1
@ -13509,6 +13509,13 @@ __metadata:
languageName: node
linkType: hard
"@types/remove-markdown@npm:^0.3.1":
version: 0.3.1
resolution: "@types/remove-markdown@npm:0.3.1"
checksum: a673aafa3a3722d959b4ad1d4e95b33f04f037a8bb95a82e843d6005b12b51f9b1bfd5954cd5c8707fbbb2f1d8865f7e632f02f99389ab83d0dcfd2cb1920dbb
languageName: node
linkType: hard
"@types/resolve@npm:1.20.2":
version: 1.20.2
resolution: "@types/resolve@npm:1.20.2"
@ -13650,7 +13657,7 @@ __metadata:
languageName: node
linkType: hard
"@types/unist@npm:*, @types/unist@npm:^2.0.0, @types/unist@npm:^2.0.2, @types/unist@npm:^2.0.3, @types/unist@npm:^2.0.6":
"@types/unist@npm:*, @types/unist@npm:^2.0.0, @types/unist@npm:^2.0.2, @types/unist@npm:^2.0.3":
version: 2.0.6
resolution: "@types/unist@npm:2.0.6"
checksum: 25cb860ff10dde48b54622d58b23e66214211a61c84c0f15f88d38b61aa1b53d4d46e42b557924a93178c501c166aa37e28d7f6d994aba13d24685326272d5db
@ -35257,6 +35264,13 @@ __metadata:
languageName: node
linkType: hard
"remove-markdown@npm:^0.5.0":
version: 0.5.0
resolution: "remove-markdown@npm:0.5.0"
checksum: c3c9051e7e7d7c5560e7d70a5093704d868fa57d244eb89533fe7bf960bce68e7901197616c6b296cb22f47af6c15a6bce693467f35709fe399379ea1125e536
languageName: node
linkType: hard
"remove-trailing-separator@npm:^1.0.1":
version: 1.1.0
resolution: "remove-trailing-separator@npm:1.1.0"
@ -37589,17 +37603,6 @@ __metadata:
languageName: node
linkType: hard
"strip-markdown@npm:^5.0.0":
version: 5.0.0
resolution: "strip-markdown@npm:5.0.0"
dependencies:
"@types/mdast": ^3.0.0
"@types/unist": ^2.0.6
unified: ^10.0.0
checksum: 4c3a9140b4fff874b63fe892de2b79d18839732bde8af7ab505b59817dd4a7dcc1bae4ef7d9bd9643e910e8b982197a30ed4859b3a45c4b4d8820893910e6bcd
languageName: node
linkType: hard
"strip-outer@npm:^1.0.0":
version: 1.0.1
resolution: "strip-outer@npm:1.0.1"