From 3c723d6f5b95998a03d8e29a797319d8cadd4dbe Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Tue, 12 Mar 2024 05:40:08 +0530 Subject: [PATCH] feat: Managed Events V2 (#12320) * init * Fix event limit interlinked switches * wip * WIP * fix bugs * lock hashed link until further notice * lock private url for managed type to stop confusion * fix recurring * prettier fix * early review fixes ... * --WIP to send only changed form fields in updateMutation * WIP with some type fixes * Revert "WIP with some type fixes" This reverts commit 00f10b772d6d08af17e7c9bd2d3601d93035d9b3. * post merge conflict resolution fixes * further type fix * fixed test * fixing e2e tests * fix test --WIP * attempt fix test & type --WIP * fix duplicate locale en entry * fix locked state persistence * adds private URL locked/unlocked functionality * fix tests * Fix issue where locked labels showed up in non-managed events * adds e2e test step * update new test * minor fixes * fix test * address changes request Part 1 fixed width on the lock badge members default location as default selection bugfix for offset toggle duplicate within itself * fixes locked/unlocked label for child event types * enable choice of destination calendar for children * Fixes width for simple lock badge * fix type * fixes workflows list and apps for managed event type * restricts creation of managed event types to EE only * further fixes --WIP * fix unit test for handleChildrenEventTypes * fix test --WIP * fix type err --WIP * fix type err in test * fix childevent payload * fixes hashedLink bug * fix test step title * lock workflow create button when workflow is locked * fix workflow detail * Don't rely on parameter ordering, use object instead * Removed console.log * Missed one --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Alex van Andel --- .../components/eventtype/EventAdvancedTab.tsx | 116 +++++------ .../web/components/eventtype/EventAppsTab.tsx | 47 +++-- .../eventtype/EventAvailabilityTab.tsx | 23 +- .../components/eventtype/EventLimitsTab.tsx | 86 ++++++-- .../components/eventtype/EventSetupTab.tsx | 74 ++++--- .../web/components/eventtype/EventTeamTab.tsx | 3 +- .../eventtype/EventTypeSingleLayout.tsx | 15 +- .../components/eventtype/EventWebhooksTab.tsx | 13 +- .../eventtype/InstantEventController.tsx | 19 +- .../eventtype/RecurringEventController.tsx | 9 +- .../RequiresConfirmationController.tsx | 9 +- .../views/event-types-listing-view.tsx | 9 +- .../views/event-types-single-view.tsx | 81 +------ apps/web/pages/event-types/[type]/index.tsx | 2 +- .../web/playwright/managed-event-types.e2e.ts | 93 ++++++++- .../managedBooking/advancedOptions.e2e.ts | 4 +- apps/web/public/static/locales/en/common.json | 28 ++- .../test/lib/handleChildrenEventTypes.test.ts | 37 +++- .../hooks/useLockedFieldsManager.tsx | 197 ++++++++++++++++-- .../lib/handleChildrenEventTypes.ts | 31 ++- .../components/EventWorkflowsTab.tsx | 54 ++++- .../components/WorkflowDetailsPage.tsx | 19 +- .../components/CreateEventTypeDialog.tsx | 9 +- packages/features/eventtypes/lib/types.ts | 90 ++++++++ packages/prisma/zod-utils.ts | 2 + packages/ui/components/alert/Alert.tsx | 3 +- .../components/form/switch/SettingsToggle.tsx | 3 +- packages/ui/components/form/switch/Switch.tsx | 11 +- 28 files changed, 758 insertions(+), 329 deletions(-) create mode 100644 packages/features/eventtypes/lib/types.ts diff --git a/apps/web/components/eventtype/EventAdvancedTab.tsx b/apps/web/components/eventtype/EventAdvancedTab.tsx index d47609bdac..776559f9df 100644 --- a/apps/web/components/eventtype/EventAdvancedTab.tsx +++ b/apps/web/components/eventtype/EventAdvancedTab.tsx @@ -1,7 +1,7 @@ import { InfoIcon } from "lucide-react"; import dynamic from "next/dynamic"; import Link from "next/link"; -import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; +import type { EventTypeSetupProps } from "pages/event-types/[type]"; import { useEffect, useState } from "react"; import { Controller, useFormContext } from "react-hook-form"; import type { z } from "zod"; @@ -15,6 +15,7 @@ import { allowDisablingAttendeeConfirmationEmails, allowDisablingHostConfirmationEmails, } from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails"; +import type { FormValues } from "@calcom/features/eventtypes/lib/types"; import { FormBuilder } from "@calcom/features/form-builder/FormBuilder"; import type { EditableSchema } from "@calcom/features/form-builder/schema"; import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector"; @@ -55,7 +56,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick workflowOnEventType.workflow); @@ -86,6 +86,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick { !hashedUrl && setHashedUrl(generateHashedLink(formMethods.getValues("users")[0]?.id ?? team?.id)); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [formMethods.getValues("users"), hashedUrl, team?.id]); const toggleGuests = (enabled: boolean) => { @@ -108,11 +109,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick setShowEventNameTip(false); const displayDestinationCalendarSelector = - !!connectedCalendarsQuery.data?.connectedCalendars.length && !team; + !!connectedCalendarsQuery.data?.connectedCalendars.length && (!team || isChildrenManagedEventType); + const verifiedSecondaryEmails = [ { label: user?.email || "", @@ -181,6 +186,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick setShowEventNameTip((old) => !old)}> @@ -233,9 +239,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick )} - -
- - ( @@ -268,14 +270,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick onChange(e)} /> )} /> - ( @@ -283,16 +284,15 @@ export const EventAdvancedTab = ({ eventType, team }: Pick onChange(e)} /> )} /> - ( @@ -339,7 +339,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick )} /> - } - {...shouldLockDisableProps("hashedLinkCheck")} + {...shouldLockDisableProps("hashedLink")} description={t("private_link_description", { appName: APP_NAME })} checked={hashedLinkVisible} onCheckedChange={(e) => { formMethods.setValue("hashedLink", e ? hashedUrl : undefined, { shouldDirty: true }); setHashedLinkVisible(e); }}> -
- {!IS_VISUAL_REGRESSION_TESTING && ( - - - - } - /> - )} -
+ {!isManagedEventType && ( +
+ {!IS_VISUAL_REGRESSION_TESTING && ( + + + + } + /> + )} +
+ )}
- ( @@ -504,7 +504,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick onChange(e)} diff --git a/apps/web/components/eventtype/EventAppsTab.tsx b/apps/web/components/eventtype/EventAppsTab.tsx index 3612bc827a..6631248b15 100644 --- a/apps/web/components/eventtype/EventAppsTab.tsx +++ b/apps/web/components/eventtype/EventAppsTab.tsx @@ -1,6 +1,6 @@ import { Trans } from "next-i18next"; import Link from "next/link"; -import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; +import type { EventTypeSetupProps } from "pages/event-types/[type]"; import { useFormContext } from "react-hook-form"; import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext"; @@ -8,6 +8,7 @@ import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCard import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types"; import type { EventTypeAppsList } from "@calcom/app-store/utils"; import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager"; +import type { FormValues } from "@calcom/features/eventtypes/lib/types"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { Button, EmptyScreen, Alert } from "@calcom/ui"; @@ -74,11 +75,13 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { }; }; - const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager( + const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager({ eventType, - t("locked_fields_admin_description"), - t("locked_fields_member_description") - ); + translate: t, + formMethods, + }); + const appsDisableProps = shouldLockDisableProps("apps", { simple: true }); + const lockedText = appsDisableProps.isLocked ? "locked" : "unlocked"; const appsWithTeamCredentials = eventTypeApps?.items.filter((app) => app.teams.length) || []; const cardsForAppsWithTeams = appsWithTeamCredentials.map((app) => { @@ -97,7 +100,6 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { app={app} eventType={eventType} eventTypeFormMetadata={eventTypeFormMetadata} - {...shouldLockDisableProps("apps")} /> ); } @@ -121,7 +123,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { }} eventType={eventType} eventTypeFormMetadata={eventTypeFormMetadata} - {...shouldLockDisableProps("apps")} + disabled={shouldLockDisableProps("apps").disabled} /> ); } @@ -133,12 +135,28 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { <>
- {isManagedEventType && ( + {(isManagedEventType || isChildrenManagedEventType) && ( + {lockedText[0].toUpperCase()} + {lockedText.slice(1)} {isManagedEventType ? "for members" : "by team admins"} + + } + actions={
{appsDisableProps.LockedIcon}
} + message={ + + {isManagedEventType ? "Members" : "You"}{" "} + {appsDisableProps.isLocked + ? "will be able to see the active apps but will not be able to edit any app settings" + : "will be able to see the active apps and will be able to edit any app settings"} + + } /> )} {!isPending && !installedApps?.length ? ( @@ -147,9 +165,9 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { headline={t("empty_installed_apps_headline")} description={t("empty_installed_apps_description")} buttonRaw={ - isChildrenManagedEventType && !isManagedEventType ? ( + appsDisableProps.disabled ? ( ) : (
- {!shouldLockDisableProps("apps").disabled && ( + {!appsDisableProps.disabled && (
{!isPending && notInstalledApps?.length ? ( <> diff --git a/apps/web/components/eventtype/EventAvailabilityTab.tsx b/apps/web/components/eventtype/EventAvailabilityTab.tsx index a992bf4942..df9b465afe 100644 --- a/apps/web/components/eventtype/EventAvailabilityTab.tsx +++ b/apps/web/components/eventtype/EventAvailabilityTab.tsx @@ -1,4 +1,4 @@ -import type { EventTypeSetup, FormValues } from "pages/event-types/[type]"; +import type { EventTypeSetup } from "pages/event-types/[type]"; import { useState, memo, useEffect } from "react"; import { Controller, useFormContext } from "react-hook-form"; import type { OptionProps, SingleValueProps } from "react-select"; @@ -6,6 +6,7 @@ import { components } from "react-select"; import dayjs from "@calcom/dayjs"; import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager"; +import type { AvailabilityOption, FormValues } from "@calcom/features/eventtypes/lib/types"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { weekdayNames } from "@calcom/lib/weekday"; @@ -17,13 +18,6 @@ import { ExternalLink, Globe } from "@calcom/ui/components/icon"; import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader"; -export type AvailabilityOption = { - label: string; - value: number; - isDefault: boolean; - isManaged?: boolean; -}; - const Option = ({ ...props }: OptionProps) => { const { label, isDefault, isManaged = false } = props.data; const { t } = useLocale(); @@ -160,12 +154,10 @@ EventTypeScheduleDetails.displayName = "EventTypeScheduleDetails"; const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => { const { t } = useLocale(); - const { shouldLockIndicator, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager( - eventType, - t("locked_fields_admin_description"), - t("locked_fields_member_description") - ); - const { watch, setValue, getValues } = useFormContext(); + const formMethods = useFormContext(); + const { shouldLockIndicator, shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = + useLockedFieldsManager({ eventType, translate: t, formMethods }); + const { watch, setValue, getValues } = formMethods; const watchSchedule = watch("schedule"); const [options, setOptions] = useState([]); @@ -246,7 +238,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
{isPending && } {!isPending && ( @@ -257,6 +249,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => { { formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null, { shouldDirty: true, @@ -257,6 +289,7 @@ export const EventLimitsTab = () => { toggleSwitchAtTheEnd={true} labelClassName="text-sm" title={t("limit_booking_frequency")} + {...bookingLimitsLocked} description={t("limit_booking_frequency_description")} checked={isChecked} onCheckedChange={(active) => { @@ -278,7 +311,12 @@ export const EventLimitsTab = () => { )} childrenClassName="lg:ml-0">
- +
); @@ -295,6 +333,7 @@ export const EventLimitsTab = () => { title={t("limit_booking_only_first_slot")} description={t("limit_booking_only_first_slot_description")} checked={isChecked} + {...onlyFirstAvailableSlotLocked} onCheckedChange={(active) => { onChange(active ?? false); }} @@ -321,6 +360,7 @@ export const EventLimitsTab = () => { childrenClassName="lg:ml-0" title={t("limit_total_booking_duration")} description={t("limit_total_booking_duration_description")} + {...durationLimitsLocked} checked={isChecked} onCheckedChange={(active) => { if (active) { @@ -335,6 +375,7 @@ export const EventLimitsTab = () => { @@ -359,6 +400,7 @@ export const EventLimitsTab = () => { childrenClassName="lg:ml-0" title={t("limit_future_bookings")} description={t("limit_future_bookings_description")} + {...periodTypeLocked} checked={isChecked} onCheckedChange={(bool) => { if (bool && !formMethods.getValues("periodDays")) { @@ -372,7 +414,9 @@ export const EventLimitsTab = () => { onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType, { shouldDirty: true }) }> - {PERIOD_TYPES.map((period) => { + {PERIOD_TYPES.filter((opt) => + periodTypeLocked.disabled ? watchPeriodType === opt.type : true + ).map((period) => { if (period.type === "UNLIMITED") return null; return (
{ watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30" )} key={period.type}> - - - + {!periodTypeLocked.disabled && ( + + + + )} {period.prefix ? {period.prefix}  : null} {period.type === "ROLLING" && ( @@ -396,18 +442,20 @@ export const EventLimitsTab = () => { type="number" className="border-default my-0 block w-16 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2" placeholder="30" + disabled={periodTypeLocked.disabled} {...formMethods.register("periodDays", { valueAsNumber: true })} />