diff --git a/apps/web/components/eventtype/EventSetupTab.tsx b/apps/web/components/eventtype/EventSetupTab.tsx
index 222a8312d5..82ba7366da 100644
--- a/apps/web/components/eventtype/EventSetupTab.tsx
+++ b/apps/web/components/eventtype/EventSetupTab.tsx
@@ -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", {
diff --git a/apps/web/components/eventtype/EventTypeSingleLayout.tsx b/apps/web/components/eventtype/EventTypeSingleLayout.tsx
index 66b0ff7667..72a1778f4f 100644
--- a/apps/web/components/eventtype/EventTypeSingleLayout.tsx
+++ b/apps/web/components/eventtype/EventTypeSingleLayout.tsx
@@ -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",
diff --git a/apps/web/pages/auth/setup/index.tsx b/apps/web/pages/auth/setup/index.tsx
index 80214751d2..f9c3074a36 100644
--- a/apps/web/pages/auth/setup/index.tsx
+++ b/apps/web/pages/auth/setup/index.tsx
@@ -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: {
diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx
index d5ef9e2ca5..d292055181 100644
--- a/apps/web/pages/event-types/index.tsx
+++ b/apps/web/pages/event-types/index.tsx
@@ -490,18 +490,20 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{t("duplicate")}
-
-
- {t("embed")}
-
-
>
)}
+ {!isManagedEventType && (
+
+
+ {t("embed")}
+
+
+ )}
{/* 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 && (
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index e3bd64ba40..529d56c259 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -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",
diff --git a/apps/web/test/lib/handleChildrenEventTypes.test.ts b/apps/web/test/lib/handleChildrenEventTypes.test.ts
index e88e89cc9c..d02d168100 100644
--- a/apps/web/test/lib/handleChildrenEventTypes.test.ts
+++ b/apps/web/test/lib/handleChildrenEventTypes.test.ts
@@ -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,
+ },
+ },
+ });
+ });
+ });
});
diff --git a/packages/features/calendars/CalendarSwitch.tsx b/packages/features/calendars/CalendarSwitch.tsx
index 8836aef5f3..bce2cd7b24 100644
--- a/packages/features/calendars/CalendarSwitch.tsx
+++ b/packages/features/calendars/CalendarSwitch.tsx
@@ -72,7 +72,7 @@ const CalendarSwitch = (props: ICalendarSwitchProps) => {
}
);
return (
-
+
!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: {
diff --git a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx
index 7c742a43cf..6a1614302a 100644
--- a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx
+++ b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx
@@ -23,6 +23,7 @@ type ItemProps = {
id: number;
title: string;
};
+ isChildrenManagedEventType: boolean;
};
const WorkflowListItem = (props: ItemProps) => {
@@ -131,18 +132,33 @@ const WorkflowListItem = (props: ItemProps) => {
})}
-
-
-
-
-
-
-
+ {!workflow.readOnly && (
+
+
+
+
+
+ )}
+
+
+ {workflow.readOnly && props.isChildrenManagedEventType && (
+
+ )}
{
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>([]);
@@ -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 ?? true,
+ } 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 (
{!isLoading ? (
@@ -217,6 +236,7 @@ function EventWorkflowsTab(props: Props) {
{isManagedEventType && (
@@ -226,7 +246,12 @@ function EventWorkflowsTab(props: Props) {
{sortedWorkflows.map((workflow) => {
return (
-
+
);
})}
@@ -238,19 +263,13 @@ function EventWorkflowsTab(props: Props) {
headline={t("workflows")}
description={t("no_workflows_description")}
buttonRaw={
- isChildrenManagedEventType && !isManagedEventType ? (
-
- ) : (
-
- )
+
}
/>
diff --git a/packages/features/ee/workflows/components/TimeTimeUnitInput.tsx b/packages/features/ee/workflows/components/TimeTimeUnitInput.tsx
index d5e8e35611..baa797e90a 100644
--- a/packages/features/ee/workflows/components/TimeTimeUnitInput.tsx
+++ b/packages/features/ee/workflows/components/TimeTimeUnitInput.tsx
@@ -17,6 +17,7 @@ import type { FormValues } from "../pages/workflow";
type Props = {
form: UseFormReturn;
+ 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 })}
diff --git a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx
index 6a8bb886eb..85da5b44ce 100644
--- a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx
+++ b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx
@@ -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>;
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) {
-
+
-
+ {!props.readOnly && (
+
+ )}
@@ -154,7 +158,7 @@ export default function WorkflowDetailsPage(props: Props) {
{form.getValues("trigger") && (
-
+
)}
{form.getValues("steps") && (
@@ -168,23 +172,28 @@ export default function WorkflowDetailsPage(props: Props) {
reload={reload}
setReload={setReload}
teamId={teamId}
+ readOnly={props.readOnly}
/>
);
})}
>
)}
-
-
-
-
+ {!props.readOnly && (
+ <>
+
+
+
+
+ >
+ )}
{workflow.activeOn && workflow.activeOn.length > 0 ? (
(
- {activeOn.eventType.title}
- ))}>
+ content={workflow.activeOn
+ .filter((wf) => (workflow.teamId ? wf.eventType.parentId === null : true))
+ .map((activeOn, key) => (
+
+ {activeOn.eventType.title}
+ {activeOn.eventType._count.children > 0
+ ? ` (+${activeOn.eventType._count.children})`
+ : ""}
+
+ ))}>
- {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,
+ })}
) : (
diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx
index 4b5e9dce11..7135b6891c 100644
--- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx
+++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx
@@ -52,6 +52,7 @@ type WorkflowStepProps = {
reload?: boolean;
setReload?: Dispatch>;
teamId?: number;
+ readOnly: boolean;
};
export default function WorkflowStepContainer(props: WorkflowStepProps) {
@@ -249,6 +250,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
@@ -325,39 +329,41 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
-
-
-
-
-
-
-
- {
- 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")}
-
-
-
-
-
+ {!props.readOnly && (
+
+
+
+
+
+
+
+ {
+ 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")}
+
+
+
+
+
+ )}
@@ -370,6 +376,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
) : (
- <>
-
- {
- setVerificationCode(e.target.value);
- }}
- required
- />
-
-
- {form.formState.errors.steps &&
- form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
-
- {form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
-
- )}
- >
+ !props.readOnly && (
+ <>
+
+ {
+ setVerificationCode(e.target.value);
+ }}
+ required
+ />
+
+
+ {form.formState.errors.steps &&
+ form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
+
+ {form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
+
+ )}
+ >
+ )
)}
)}
@@ -551,6 +562,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
@@ -566,6 +578,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
@@ -580,6 +593,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
control={form.control}
render={() => (
@@ -611,6 +626,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {