Skip to content
This repository was archived by the owner on Mar 3, 2026. It is now read-only.

Commit 27512b1

Browse files
CarinaWollihariombalharaCarinaWollizomarsPeerRich
committed
Team Workflows (calcom#7038)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com>
1 parent 7b47956 commit 27512b1

27 files changed

Lines changed: 1215 additions & 472 deletions

File tree

apps/web/pages/event-types/index.tsx

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ import type { FC } from "react";
55
import { useEffect, useState, memo } from "react";
66
import { z } from "zod";
77

8-
import {
9-
CreateEventTypeButton,
10-
EventTypeDescriptionLazy as EventTypeDescription,
11-
} from "@calcom/features/eventtypes/components";
8+
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
9+
import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
10+
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
1211
import Shell from "@calcom/features/shell/Shell";
1312
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
1413
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -35,6 +34,7 @@ import {
3534
showToast,
3635
Switch,
3736
Tooltip,
37+
CreateButton,
3838
HorizontalTabs,
3939
} from "@calcom/ui";
4040
import {
@@ -134,9 +134,9 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
134134
</small>
135135
) : null}
136136
{readOnly && (
137-
<span className="items-center rounded-sm bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800 ltr:ml-2 ltr:mr-2 rtl:ml-2">
137+
<Badge variant="gray" className="ml-2">
138138
{t("readonly")}
139-
</span>
139+
</Badge>
140140
)}
141141
</div>
142142
<EventTypeDescription
@@ -240,7 +240,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
240240
const openDuplicateModal = (eventType: EventType, group: EventTypeGroup) => {
241241
const query = {
242242
...router.query,
243-
dialog: "duplicate-event-type",
243+
dialog: "duplicate",
244244
title: eventType.title,
245245
description: eventType.description,
246246
slug: eventType.slug,
@@ -627,17 +627,32 @@ const CreateFirstEventTypeView = () => {
627627
};
628628

629629
const CTA = () => {
630+
const { t } = useLocale();
631+
630632
const query = trpc.viewer.eventTypes.getByViewer.useQuery();
631633

632634
if (!query.data) return null;
633635

634-
return <CreateEventTypeButton canAddEvents={true} options={query.data.profiles} />;
636+
const profileOptions = query.data.profiles
637+
.filter((profile) => !profile.readOnly)
638+
.map((profile) => {
639+
return { teamId: profile.teamId, label: profile.name || profile.slug, image: profile.image };
640+
});
641+
642+
return (
643+
<CreateButton
644+
subtitle={t("create_event_on").toUpperCase()}
645+
options={profileOptions}
646+
createDialog={CreateEventTypeDialog}
647+
/>
648+
);
635649
};
636650

637651
const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer);
638652

639653
const EventTypesPage = () => {
640654
const { t } = useLocale();
655+
const router = useRouter();
641656

642657
const isMobile = useMediaQuery("(max-width: 768px)");
643658

@@ -683,6 +698,7 @@ const EventTypesPage = () => {
683698
)}
684699

685700
<EmbedDialog />
701+
{router.query.dialog === "duplicate" && <DuplicateDialog />}
686702
</>
687703
)}
688704
/>

apps/web/public/static/locales/en/common.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,6 @@
598598
"new_event_type_heading": "Create your first event type",
599599
"new_event_type_description": "Event types enable you to share links that show available times on your calendar and allow people to make bookings with you.",
600600
"new_event_title": "Add a new event type",
601-
"new_event_subtitle": "Create an event type under your name or a team.",
602601
"new_team_event": "Add a new team event type",
603602
"new_event_description": "Create a new event type for people to book times with.",
604603
"event_type_created_successfully": "{{eventTypeTitle}} event type created successfully",
@@ -1527,6 +1526,7 @@
15271526
"sender_name": "Sender name",
15281527
"already_invited": "Attendee already invited",
15291528
"no_recordings_found": "No recordings found",
1529+
"new_workflow_subtitle": "New workflow for...",
15301530
"reporting": "Reporting",
15311531
"reporting_feature": "See all incoming from data and download it as a CSV",
15321532
"teams_plan_required": "Teams plan required",
@@ -1581,14 +1581,17 @@
15811581
"email_no_user_step_four":"Join {{teamName}}",
15821582
"email_no_user_signoff":"Happy Scheduling from the {{appName}} team",
15831583
"impersonation_user_tip": "You are about to impersonate a user, which means you can make changes on their behalf. Please be careful.",
1584-
"scheduler": "{Scheduler}",
15851584
"available_variables": "Available variables",
1585+
"scheduler": "{Scheduler}",
1586+
"no_workflows": "No workflows",
1587+
"change_filter": "Change filter to see your personal and team workflows.",
15861588
"recommended_next_steps": "Recommended next steps",
15871589
"create_a_managed_event": "Create a managed event type",
15881590
"meetings_are_better_with_the_right": "Meetings are better with the right team members there. Invite them now.",
15891591
"create_a_one_one_template": "Create a one-one one template for an event type and distribute it to multiple members.",
15901592
"collective_or_roundrobin": "Collective or round-robin",
15911593
"book_your_team_members": "Book your team members together with collective events or cycle through to get the right person with round-robin.",
1594+
"create_event_on": "Create event on",
15921595
"default_app_link_title":"Set a default app link",
15931596
"default_app_link_description":"Setting a default app link allows all newly created event types to use the app link you set.",
15941597
"change_default_conferencing_app":"Set as default",
@@ -1619,5 +1622,7 @@
16191622
"select_a_router": "Select a router",
16201623
"add_a_new_route": "Add a new Route",
16211624
"no_responses_yet": "No responses yet",
1622-
"this_will_be_the_placeholder": "This will be the placeholder"
1625+
"this_will_be_the_placeholder": "This will be the placeholder",
1626+
"verification_code": "Verification code",
1627+
"verify": "Verify"
16231628
}

packages/features/ee/teams/pages/team-profile-view.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { zodResolver } from "@hookform/resolvers/zod";
2-
import { MembershipRole, Prisma } from "@prisma/client";
2+
import type { Prisma } from "@prisma/client";
3+
import { MembershipRole } from "@prisma/client";
34
import MarkdownIt from "markdown-it";
45
import { useSession } from "next-auth/react";
56
import Link from "next/link";
67
import { useRouter } from "next/router";
78
import { Controller, useForm } from "react-hook-form";
89
import { z } from "zod";
910

10-
import { CAL_URL } from "@calcom/lib/constants";
1111
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
1212
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
1313
import { useLocale } from "@calcom/lib/hooks/useLocale";

packages/features/ee/workflows/components/DeleteDialog.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Dispatch, SetStateAction } from "react";
1+
import type { Dispatch, SetStateAction } from "react";
22

33
import { useLocale } from "@calcom/lib/hooks/useLocale";
44
import { HttpError } from "@calcom/lib/http-error";
@@ -30,6 +30,10 @@ export const DeleteDialog = (props: IDeleteDialog) => {
3030
showToast(message, "error");
3131
setIsOpenDialog(false);
3232
}
33+
if (err.data?.code === "UNAUTHORIZED") {
34+
const message = `${err.data.code}: You are not authorized to delete this workflow`;
35+
showToast(message, "error");
36+
}
3337
},
3438
});
3539

Lines changed: 53 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import React from "react";
1+
import { useRouter } from "next/router";
22

33
import { useLocale } from "@calcom/lib/hooks/useLocale";
4-
import { SVGComponent } from "@calcom/types/SVGComponent";
5-
import { Button } from "@calcom/ui";
6-
import { FiSmartphone, FiMail, FiPlus } from "@calcom/ui/components/icon";
4+
import { HttpError } from "@calcom/lib/http-error";
5+
import { trpc } from "@calcom/trpc/react";
6+
import type { SVGComponent } from "@calcom/types/SVGComponent";
7+
import { CreateButton, showToast, EmptyScreen as ClassicEmptyScreen } from "@calcom/ui";
8+
import { FiSmartphone, FiMail, FiZap } from "@calcom/ui/components/icon";
79

810
type WorkflowExampleType = {
911
Icon: SVGComponent;
@@ -31,24 +33,33 @@ function WorkflowExample(props: WorkflowExampleType) {
3133
);
3234
}
3335

34-
export default function EmptyScreen({
35-
IconHeading,
36-
headline,
37-
description,
38-
buttonText,
39-
buttonOnClick,
40-
isLoading,
41-
showExampleWorkflows,
42-
}: {
43-
IconHeading: SVGComponent;
44-
headline: string;
45-
description: string | React.ReactElement;
46-
buttonText?: string;
47-
buttonOnClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
48-
isLoading: boolean;
49-
showExampleWorkflows: boolean;
36+
export default function EmptyScreen(props: {
37+
profileOptions: {
38+
label: string | null;
39+
image?: string | null;
40+
teamId: number | null | undefined;
41+
}[];
42+
isFilteredView: boolean;
5043
}) {
5144
const { t } = useLocale();
45+
const router = useRouter();
46+
47+
const createMutation = trpc.viewer.workflows.create.useMutation({
48+
onSuccess: async ({ workflow }) => {
49+
await router.replace("/workflows/" + workflow.id);
50+
},
51+
onError: (err) => {
52+
if (err instanceof HttpError) {
53+
const message = `${err.statusCode}: ${err.message}`;
54+
showToast(message, "error");
55+
}
56+
57+
if (err.data?.code === "UNAUTHORIZED") {
58+
const message = `${err.data.code}: You are not authorized to create this workflow`;
59+
showToast(message, "error");
60+
}
61+
},
62+
});
5263

5364
const workflowsExamples = [
5465
{ icon: FiSmartphone, text: t("workflow_example_1") },
@@ -60,38 +71,39 @@ export default function EmptyScreen({
6071
];
6172
// new workflow example when 'after meetings ends' trigger is implemented: Send custom thank you email to attendee after event (FiSmile icon),
6273

74+
if (props.isFilteredView) {
75+
return <ClassicEmptyScreen Icon={FiZap} headline={t("no_workflows")} description={t("change_filter")} />;
76+
}
77+
6378
return (
6479
<>
6580
<div className="min-h-80 flex w-full flex-col items-center justify-center rounded-md ">
6681
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-200 dark:bg-white">
67-
<IconHeading className="inline-block h-10 w-10 stroke-[1.3px] dark:bg-gray-900 dark:text-gray-600" />
82+
<FiZap className="inline-block h-10 w-10 stroke-[1.3px] dark:bg-gray-900 dark:text-gray-600" />
6883
</div>
6984
<div className="max-w-[420px] text-center">
70-
<h2 className="text-semibold font-cal mt-6 text-xl dark:text-gray-300">{headline}</h2>
85+
<h2 className="text-semibold font-cal mt-6 text-xl dark:text-gray-300">{t("workflows")}</h2>
7186
<p className="line-clamp-2 mt-3 text-sm font-normal leading-6 text-gray-700 dark:text-gray-300">
72-
{description}
87+
{t("no_workflows_description")}
7388
</p>
74-
{buttonOnClick && buttonText && (
75-
<Button
76-
type="button"
77-
StartIcon={FiPlus}
78-
onClick={(e) => buttonOnClick(e)}
79-
loading={isLoading}
80-
className="mx-auto mt-8">
81-
{buttonText}
82-
</Button>
83-
)}
89+
<div className="mt-8 ">
90+
<CreateButton
91+
subtitle={t("new_workflow_subtitle").toUpperCase()}
92+
options={props.profileOptions}
93+
createFunction={(teamId?: number) => createMutation.mutate({ teamId })}
94+
buttonText={t("create_workflow")}
95+
isLoading={createMutation.isLoading}
96+
/>
97+
</div>
8498
</div>
8599
</div>
86-
{showExampleWorkflows && (
87-
<div className="flex flex-row items-center justify-center">
88-
<div className="grid-cols-none items-center lg:grid lg:grid-cols-3 xl:mx-20">
89-
{workflowsExamples.map((example, index) => (
90-
<WorkflowExample key={index} Icon={example.icon} text={example.text} />
91-
))}
92-
</div>
100+
<div className="flex flex-row items-center justify-center">
101+
<div className="grid-cols-none items-center lg:grid lg:grid-cols-3 xl:mx-20">
102+
{workflowsExamples.map((example, index) => (
103+
<WorkflowExample key={index} Icon={example.icon} text={example.text} />
104+
))}
93105
</div>
94-
)}
106+
</div>
95107
</>
96108
);
97109
}

packages/features/ee/workflows/components/EventWorkflowsTab.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { FiExternalLink, FiZap } from "@calcom/ui/components/icon";
1313
import LicenseRequired from "../../common/components/v2/LicenseRequired";
1414
import { getActionIcon } from "../lib/getActionIcon";
1515
import SkeletonLoader from "./SkeletonLoaderEventWorkflowsTab";
16-
import { WorkflowType } from "./WorkflowListPage";
16+
import type { WorkflowType } from "./WorkflowListPage";
1717

1818
type ItemProps = {
1919
workflow: WorkflowType;
@@ -65,6 +65,11 @@ const WorkflowListItem = (props: ItemProps) => {
6565
const message = `${err.statusCode}: ${err.message}`;
6666
showToast(message, "error");
6767
}
68+
if (err.data?.code === "UNAUTHORIZED") {
69+
// TODO: Add missing translation
70+
const message = `${err.data.code}: You are not authorized to enable or disable this workflow`;
71+
showToast(message, "error");
72+
}
6873
},
6974
});
7075

@@ -148,14 +153,21 @@ type Props = {
148153
eventType: {
149154
id: number;
150155
title: string;
156+
userId: number | null;
157+
team: {
158+
id?: number;
159+
} | null;
151160
};
152161
workflows: WorkflowType[];
153162
};
154163

155164
function EventWorkflowsTab(props: Props) {
156-
const { workflows } = props;
165+
const { workflows, eventType } = props;
157166
const { t } = useLocale();
158-
const { data, isLoading } = trpc.viewer.workflows.list.useQuery();
167+
const { data, isLoading } = trpc.viewer.workflows.list.useQuery({
168+
teamId: eventType.team?.id,
169+
userId: eventType.userId || undefined,
170+
});
159171
const router = useRouter();
160172
const [sortedWorkflows, setSortedWorkflows] = useState<Array<WorkflowType>>([]);
161173

@@ -176,7 +188,7 @@ function EventWorkflowsTab(props: Props) {
176188
}
177189
}, [isLoading]);
178190

179-
const createMutation = trpc.viewer.workflows.createV2.useMutation({
191+
const createMutation = trpc.viewer.workflows.create.useMutation({
180192
onSuccess: async ({ workflow }) => {
181193
await router.replace("/workflows/" + workflow.id);
182194
},
@@ -212,7 +224,7 @@ function EventWorkflowsTab(props: Props) {
212224
<Button
213225
target="_blank"
214226
color="secondary"
215-
onClick={() => createMutation.mutate()}
227+
onClick={() => createMutation.mutate({ teamId: eventType.team?.id })}
216228
loading={createMutation.isLoading}>
217229
{t("create_workflow")}
218230
</Button>

0 commit comments

Comments
 (0)