Team Workflows (#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>
This commit is contained in:
co-authored by
Hariom Balhara
CarinaWolli
zomars
Peer Richelsen
parent
c20835a4c8
commit
0ec71e52ef
@@ -1,4 +1,4 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
@@ -30,6 +30,10 @@ export const DeleteDialog = (props: IDeleteDialog) => {
|
||||
showToast(message, "error");
|
||||
setIsOpenDialog(false);
|
||||
}
|
||||
if (err.data?.code === "UNAUTHORIZED") {
|
||||
const message = `${err.data.code}: You are not authorized to delete this workflow`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SVGComponent } from "@calcom/types/SVGComponent";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { FiSmartphone, FiMail, FiPlus } from "@calcom/ui/components/icon";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { SVGComponent } from "@calcom/types/SVGComponent";
|
||||
import { CreateButton, showToast, EmptyScreen as ClassicEmptyScreen } from "@calcom/ui";
|
||||
import { FiSmartphone, FiMail, FiZap } from "@calcom/ui/components/icon";
|
||||
|
||||
type WorkflowExampleType = {
|
||||
Icon: SVGComponent;
|
||||
@@ -31,24 +33,33 @@ function WorkflowExample(props: WorkflowExampleType) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function EmptyScreen({
|
||||
IconHeading,
|
||||
headline,
|
||||
description,
|
||||
buttonText,
|
||||
buttonOnClick,
|
||||
isLoading,
|
||||
showExampleWorkflows,
|
||||
}: {
|
||||
IconHeading: SVGComponent;
|
||||
headline: string;
|
||||
description: string | React.ReactElement;
|
||||
buttonText?: string;
|
||||
buttonOnClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
isLoading: boolean;
|
||||
showExampleWorkflows: boolean;
|
||||
export default function EmptyScreen(props: {
|
||||
profileOptions: {
|
||||
label: string | null;
|
||||
image?: string | null;
|
||||
teamId: number | null | undefined;
|
||||
}[];
|
||||
isFilteredView: boolean;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
const createMutation = trpc.viewer.workflows.create.useMutation({
|
||||
onSuccess: async ({ workflow }) => {
|
||||
await router.replace("/workflows/" + workflow.id);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
|
||||
if (err.data?.code === "UNAUTHORIZED") {
|
||||
const message = `${err.data.code}: You are not authorized to create this workflow`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const workflowsExamples = [
|
||||
{ icon: FiSmartphone, text: t("workflow_example_1") },
|
||||
@@ -60,38 +71,39 @@ export default function EmptyScreen({
|
||||
];
|
||||
// new workflow example when 'after meetings ends' trigger is implemented: Send custom thank you email to attendee after event (FiSmile icon),
|
||||
|
||||
if (props.isFilteredView) {
|
||||
return <ClassicEmptyScreen Icon={FiZap} headline={t("no_workflows")} description={t("change_filter")} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-80 flex w-full flex-col items-center justify-center rounded-md ">
|
||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-200 dark:bg-white">
|
||||
<IconHeading className="inline-block h-10 w-10 stroke-[1.3px] dark:bg-gray-900 dark:text-gray-600" />
|
||||
<FiZap className="inline-block h-10 w-10 stroke-[1.3px] dark:bg-gray-900 dark:text-gray-600" />
|
||||
</div>
|
||||
<div className="max-w-[420px] text-center">
|
||||
<h2 className="text-semibold font-cal mt-6 text-xl dark:text-gray-300">{headline}</h2>
|
||||
<h2 className="text-semibold font-cal mt-6 text-xl dark:text-gray-300">{t("workflows")}</h2>
|
||||
<p className="line-clamp-2 mt-3 text-sm font-normal leading-6 text-gray-700 dark:text-gray-300">
|
||||
{description}
|
||||
{t("no_workflows_description")}
|
||||
</p>
|
||||
{buttonOnClick && buttonText && (
|
||||
<Button
|
||||
type="button"
|
||||
StartIcon={FiPlus}
|
||||
onClick={(e) => buttonOnClick(e)}
|
||||
loading={isLoading}
|
||||
className="mx-auto mt-8">
|
||||
{buttonText}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showExampleWorkflows && (
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<div className="grid-cols-none items-center lg:grid lg:grid-cols-3 xl:mx-20">
|
||||
{workflowsExamples.map((example, index) => (
|
||||
<WorkflowExample key={index} Icon={example.icon} text={example.text} />
|
||||
))}
|
||||
<div className="mt-8 ">
|
||||
<CreateButton
|
||||
subtitle={t("new_workflow_subtitle").toUpperCase()}
|
||||
options={props.profileOptions}
|
||||
createFunction={(teamId?: number) => createMutation.mutate({ teamId })}
|
||||
buttonText={t("create_workflow")}
|
||||
isLoading={createMutation.isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<div className="grid-cols-none items-center lg:grid lg:grid-cols-3 xl:mx-20">
|
||||
{workflowsExamples.map((example, index) => (
|
||||
<WorkflowExample key={index} Icon={example.icon} text={example.text} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { FiExternalLink, FiZap } from "@calcom/ui/components/icon";
|
||||
import LicenseRequired from "../../common/components/v2/LicenseRequired";
|
||||
import { getActionIcon } from "../lib/getActionIcon";
|
||||
import SkeletonLoader from "./SkeletonLoaderEventWorkflowsTab";
|
||||
import { WorkflowType } from "./WorkflowListPage";
|
||||
import type { WorkflowType } from "./WorkflowListPage";
|
||||
|
||||
type ItemProps = {
|
||||
workflow: WorkflowType;
|
||||
@@ -65,6 +65,11 @@ const WorkflowListItem = (props: ItemProps) => {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
if (err.data?.code === "UNAUTHORIZED") {
|
||||
// TODO: Add missing translation
|
||||
const message = `${err.data.code}: You are not authorized to enable or disable this workflow`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -148,14 +153,21 @@ type Props = {
|
||||
eventType: {
|
||||
id: number;
|
||||
title: string;
|
||||
userId: number | null;
|
||||
team: {
|
||||
id?: number;
|
||||
} | null;
|
||||
};
|
||||
workflows: WorkflowType[];
|
||||
};
|
||||
|
||||
function EventWorkflowsTab(props: Props) {
|
||||
const { workflows } = props;
|
||||
const { workflows, eventType } = props;
|
||||
const { t } = useLocale();
|
||||
const { data, isLoading } = trpc.viewer.workflows.list.useQuery();
|
||||
const { data, isLoading } = trpc.viewer.workflows.list.useQuery({
|
||||
teamId: eventType.team?.id,
|
||||
userId: eventType.userId || undefined,
|
||||
});
|
||||
const router = useRouter();
|
||||
const [sortedWorkflows, setSortedWorkflows] = useState<Array<WorkflowType>>([]);
|
||||
|
||||
@@ -176,7 +188,7 @@ function EventWorkflowsTab(props: Props) {
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
const createMutation = trpc.viewer.workflows.createV2.useMutation({
|
||||
const createMutation = trpc.viewer.workflows.create.useMutation({
|
||||
onSuccess: async ({ workflow }) => {
|
||||
await router.replace("/workflows/" + workflow.id);
|
||||
},
|
||||
@@ -212,7 +224,7 @@ function EventWorkflowsTab(props: Props) {
|
||||
<Button
|
||||
target="_blank"
|
||||
color="secondary"
|
||||
onClick={() => createMutation.mutate()}
|
||||
onClick={() => createMutation.mutate({ teamId: eventType.team?.id })}
|
||||
loading={createMutation.isLoading}>
|
||||
{t("create_workflow")}
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { WorkflowActions, WorkflowTemplates } from "@prisma/client";
|
||||
import type { WorkflowActions } from "@prisma/client";
|
||||
import { WorkflowTemplates } from "@prisma/client";
|
||||
import { useRouter } from "next/router";
|
||||
import { Dispatch, SetStateAction, useMemo, useState } from "react";
|
||||
import { Controller, UseFormReturn } from "react-hook-form";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { Controller } from "react-hook-form";
|
||||
|
||||
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -21,10 +24,12 @@ interface Props {
|
||||
workflowId: number;
|
||||
selectedEventTypes: Option[];
|
||||
setSelectedEventTypes: Dispatch<SetStateAction<Option[]>>;
|
||||
teamId?: number;
|
||||
isMixedEventType: boolean;
|
||||
}
|
||||
|
||||
export default function WorkflowDetailsPage(props: Props) {
|
||||
const { form, workflowId, selectedEventTypes, setSelectedEventTypes } = props;
|
||||
const { form, workflowId, selectedEventTypes, setSelectedEventTypes, teamId, isMixedEventType } = props;
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -36,19 +41,32 @@ export default function WorkflowDetailsPage(props: Props) {
|
||||
|
||||
const eventTypeOptions = useMemo(
|
||||
() =>
|
||||
data?.eventTypeGroups.reduce(
|
||||
(options, group) => [
|
||||
data?.eventTypeGroups.reduce((options, group) => {
|
||||
/** only show event types that belong to team or user */
|
||||
if (!(!teamId && !group.teamId) || teamId !== group.teamId) return options;
|
||||
return [
|
||||
...options,
|
||||
...group.eventTypes.map((eventType) => ({
|
||||
value: String(eventType.id),
|
||||
label: eventType.title,
|
||||
})),
|
||||
],
|
||||
[] as Option[]
|
||||
) || [],
|
||||
];
|
||||
}, [] as Option[]) || [],
|
||||
[data]
|
||||
);
|
||||
|
||||
let allEventTypeOptions = eventTypeOptions;
|
||||
const distinctEventTypes = new Set();
|
||||
|
||||
if (!teamId && isMixedEventType) {
|
||||
allEventTypeOptions = [...eventTypeOptions, ...selectedEventTypes];
|
||||
allEventTypeOptions = allEventTypeOptions.filter((option) => {
|
||||
const duplicate = distinctEventTypes.has(option.value);
|
||||
distinctEventTypes.add(option.value);
|
||||
return !duplicate;
|
||||
});
|
||||
}
|
||||
|
||||
const addAction = (
|
||||
action: WorkflowActions,
|
||||
sendTo?: string,
|
||||
@@ -101,7 +119,7 @@ export default function WorkflowDetailsPage(props: Props) {
|
||||
render={() => {
|
||||
return (
|
||||
<MultiSelectCheckboxes
|
||||
options={eventTypeOptions}
|
||||
options={allEventTypeOptions}
|
||||
isLoading={isLoading}
|
||||
className="w-full md:w-64"
|
||||
setSelected={setSelectedEventTypes}
|
||||
@@ -129,7 +147,7 @@ export default function WorkflowDetailsPage(props: Props) {
|
||||
<div className="w-full rounded-md border border-gray-200 bg-gray-50 p-3 py-5 md:ml-3 md:p-8">
|
||||
{form.getValues("trigger") && (
|
||||
<div>
|
||||
<WorkflowStepContainer form={form} />
|
||||
<WorkflowStepContainer form={form} teamId={teamId} />
|
||||
</div>
|
||||
)}
|
||||
{form.getValues("steps") && (
|
||||
@@ -142,6 +160,7 @@ export default function WorkflowDetailsPage(props: Props) {
|
||||
step={step}
|
||||
reload={reload}
|
||||
setReload={setReload}
|
||||
teamId={teamId}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Workflow, WorkflowStep } from "@prisma/client";
|
||||
import type { Workflow, WorkflowStep, Membership } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Button,
|
||||
@@ -15,50 +15,22 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownItem,
|
||||
DropdownMenuTrigger,
|
||||
showToast,
|
||||
Tooltip,
|
||||
Badge,
|
||||
} from "@calcom/ui";
|
||||
import { FiEdit2, FiLink, FiMoreHorizontal, FiTrash2, FiZap } from "@calcom/ui/components/icon";
|
||||
import { FiEdit2, FiLink, FiMoreHorizontal, FiTrash2 } from "@calcom/ui/components/icon";
|
||||
|
||||
import { getActionIcon } from "../lib/getActionIcon";
|
||||
import { DeleteDialog } from "./DeleteDialog";
|
||||
import EmptyScreen from "./EmptyScreen";
|
||||
|
||||
const CreateEmptyWorkflowView = () => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
const createMutation = trpc.viewer.workflows.createV2.useMutation({
|
||||
onSuccess: async ({ workflow }) => {
|
||||
await router.replace("/workflows/" + workflow.id);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
|
||||
if (err.data?.code === "UNAUTHORIZED") {
|
||||
const message = `${err.data.code}: You are not able to create this workflow`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<EmptyScreen
|
||||
buttonText={t("create_workflow")}
|
||||
buttonOnClick={() => createMutation.mutate()}
|
||||
IconHeading={FiZap}
|
||||
headline={t("workflows")}
|
||||
description={t("no_workflows_description")}
|
||||
isLoading={createMutation.isLoading}
|
||||
showExampleWorkflows={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export type WorkflowType = Workflow & {
|
||||
team: {
|
||||
id: number;
|
||||
name: string;
|
||||
members: Membership[];
|
||||
slug: string | null;
|
||||
} | null;
|
||||
steps: WorkflowStep[];
|
||||
activeOn: {
|
||||
eventType: {
|
||||
@@ -66,16 +38,24 @@ export type WorkflowType = Workflow & {
|
||||
title: string;
|
||||
};
|
||||
}[];
|
||||
readOnly?: boolean;
|
||||
};
|
||||
interface Props {
|
||||
workflows: WorkflowType[] | undefined;
|
||||
profileOptions: {
|
||||
image?: string | null;
|
||||
label: string | null;
|
||||
teamId: number | null | undefined;
|
||||
}[];
|
||||
hasNoWorkflows?: boolean;
|
||||
}
|
||||
export default function WorkflowListPage({ workflows }: Props) {
|
||||
export default function WorkflowListPage({ workflows, profileOptions, hasNoWorkflows }: Props) {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [workflowToDeleteId, setwWorkflowToDeleteId] = useState(0);
|
||||
const router = useRouter();
|
||||
const session = useSession();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -87,57 +67,79 @@ export default function WorkflowListPage({ workflows }: Props) {
|
||||
<div className="first-line:group flex w-full items-center justify-between p-4 hover:bg-neutral-50 sm:px-6">
|
||||
<Link href={"/workflows/" + workflow.id} className="flex-grow cursor-pointer">
|
||||
<div className="rtl:space-x-reverse">
|
||||
<div
|
||||
className={classNames(
|
||||
"max-w-56 truncate text-sm font-medium leading-6 text-gray-900 md:max-w-max",
|
||||
workflow.name ? "text-gray-900" : "text-gray-500"
|
||||
)}>
|
||||
{workflow.name
|
||||
? workflow.name
|
||||
: workflow.steps[0]
|
||||
? "Untitled (" +
|
||||
`${t(`${workflow.steps[0].action.toLowerCase()}_action`)}`
|
||||
.charAt(0)
|
||||
.toUpperCase() +
|
||||
`${t(`${workflow.steps[0].action.toLowerCase()}_action`)}`.slice(1) +
|
||||
")"
|
||||
: "Untitled"}
|
||||
<div className="flex">
|
||||
<div
|
||||
className={classNames(
|
||||
"max-w-56 truncate text-sm font-medium leading-6 text-gray-900 md:max-w-max",
|
||||
workflow.name ? "text-gray-900" : "text-gray-500"
|
||||
)}>
|
||||
{workflow.name
|
||||
? workflow.name
|
||||
: workflow.steps[0]
|
||||
? "Untitled (" +
|
||||
`${t(`${workflow.steps[0].action.toLowerCase()}_action`)}`
|
||||
.charAt(0)
|
||||
.toUpperCase() +
|
||||
`${t(`${workflow.steps[0].action.toLowerCase()}_action`)}`.slice(1) +
|
||||
")"
|
||||
: "Untitled"}
|
||||
</div>
|
||||
<div>
|
||||
{workflow.readOnly && (
|
||||
<Badge variant="gray" className="ml-2 ">
|
||||
{t("readonly")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ul className="mt-2 flex flex-wrap space-x-1 sm:flex-nowrap ">
|
||||
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
|
||||
<div>
|
||||
{getActionIcon(workflow.steps)}
|
||||
|
||||
<span className="mr-1">{t("triggers")}</span>
|
||||
{workflow.timeUnit && workflow.time && (
|
||||
<span className="mr-1">
|
||||
{t(`${workflow.timeUnit.toLowerCase()}`, { count: workflow.time })}
|
||||
</span>
|
||||
)}
|
||||
<span>{t(`${workflow.trigger.toLowerCase()}_trigger`)}</span>
|
||||
</div>
|
||||
<ul className="mt-1 flex flex-wrap space-x-2 sm:flex-nowrap ">
|
||||
<li>
|
||||
<Badge variant="gray">
|
||||
<div>
|
||||
{getActionIcon(workflow.steps)}
|
||||
|
||||
<span className="mr-1">{t("triggers")}</span>
|
||||
{workflow.timeUnit && workflow.time && (
|
||||
<span className="mr-1">
|
||||
{t(`${workflow.timeUnit.toLowerCase()}`, { count: workflow.time })}
|
||||
</span>
|
||||
)}
|
||||
<span>{t(`${workflow.trigger.toLowerCase()}_trigger`)}</span>
|
||||
</div>
|
||||
</Badge>
|
||||
</li>
|
||||
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
|
||||
{workflow.activeOn && workflow.activeOn.length > 0 ? (
|
||||
<Tooltip
|
||||
content={workflow.activeOn.map((activeOn, key) => (
|
||||
<p key={key}>{activeOn.eventType.title}</p>
|
||||
))}>
|
||||
<li>
|
||||
<Badge variant="gray">
|
||||
{workflow.activeOn && workflow.activeOn.length > 0 ? (
|
||||
<Tooltip
|
||||
content={workflow.activeOn.map((activeOn, key) => (
|
||||
<p key={key}>{activeOn.eventType.title}</p>
|
||||
))}>
|
||||
<div>
|
||||
<FiLink className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||
{t("active_on_event_types", { count: workflow.activeOn.length })}
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div>
|
||||
<FiLink className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||
{t("active_on_event_types", { count: workflow.activeOn.length })}
|
||||
{t("no_active_event_types")}
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div>
|
||||
<FiLink className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||
{t("no_active_event_types")}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</Badge>
|
||||
</li>
|
||||
{workflow.teamId && (
|
||||
<li>
|
||||
<Badge variant="gray">
|
||||
<>{workflow.team?.name}</>
|
||||
</Badge>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-shrink-0">
|
||||
<div className="hidden sm:block">
|
||||
<ButtonGroup combined>
|
||||
@@ -147,6 +149,7 @@ export default function WorkflowListPage({ workflows }: Props) {
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon={FiEdit2}
|
||||
disabled={workflow.readOnly}
|
||||
onClick={async () => await router.replace("/workflows/" + workflow.id)}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -158,40 +161,48 @@ export default function WorkflowListPage({ workflows }: Props) {
|
||||
}}
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
disabled={workflow.readOnly}
|
||||
StartIcon={FiTrash2}
|
||||
/>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div className="block sm:hidden">
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="button" color="minimal" variant="icon" StartIcon={FiMoreHorizontal} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
{!workflow.readOnly && (
|
||||
<div className="block sm:hidden">
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
StartIcon={FiEdit2}
|
||||
onClick={async () => await router.replace("/workflows/" + workflow.id)}>
|
||||
{t("edit")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color="destructive"
|
||||
StartIcon={FiTrash2}
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(true);
|
||||
setwWorkflowToDeleteId(workflow.id);
|
||||
}}>
|
||||
{t("delete")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
</div>
|
||||
color="minimal"
|
||||
variant="icon"
|
||||
StartIcon={FiMoreHorizontal}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
StartIcon={FiEdit2}
|
||||
onClick={async () => await router.replace("/workflows/" + workflow.id)}>
|
||||
{t("edit")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color="destructive"
|
||||
StartIcon={FiTrash2}
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(true);
|
||||
setwWorkflowToDeleteId(workflow.id);
|
||||
}}>
|
||||
{t("delete")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -207,7 +218,7 @@ export default function WorkflowListPage({ workflows }: Props) {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<CreateEmptyWorkflowView />
|
||||
<EmptyScreen profileOptions={profileOptions} isFilteredView={!hasNoWorkflows} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import {
|
||||
TimeUnit,
|
||||
WorkflowActions,
|
||||
WorkflowStep,
|
||||
WorkflowTemplates,
|
||||
WorkflowTriggerEvents,
|
||||
} from "@prisma/client";
|
||||
import { Dispatch, SetStateAction, useRef, useState } from "react";
|
||||
import { Controller, UseFormReturn } from "react-hook-form";
|
||||
import type { WorkflowStep } from "@prisma/client";
|
||||
import { TimeUnit, WorkflowActions, WorkflowTemplates, WorkflowTriggerEvents } from "@prisma/client";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { Controller } from "react-hook-form";
|
||||
import "react-phone-number-input/style.css";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
@@ -51,17 +48,20 @@ type WorkflowStepProps = {
|
||||
form: UseFormReturn<FormValues>;
|
||||
reload?: boolean;
|
||||
setReload?: Dispatch<SetStateAction<boolean>>;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||
const { t, i18n } = useLocale();
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const { step, form, reload, setReload } = props;
|
||||
const { data: _verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery();
|
||||
const verifiedNumbers = _verifiedNumbers?.map((number) => number.phoneNumber);
|
||||
const { step, form, reload, setReload, teamId } = props;
|
||||
const { data: _verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery(
|
||||
{ teamId },
|
||||
{ enabled: !!teamId }
|
||||
);
|
||||
const verifiedNumbers = _verifiedNumbers?.map((number) => number.phoneNumber) || [];
|
||||
const [isAdditionalInputsDialogOpen, setIsAdditionalInputsDialogOpen] = useState(false);
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
|
||||
const [verificationCode, setVerificationCode] = useState("");
|
||||
|
||||
@@ -75,6 +75,13 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||
: false
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setNumberVerified(
|
||||
!!step &&
|
||||
!!verifiedNumbers.find((number) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`))
|
||||
);
|
||||
}, [verifiedNumbers.length]);
|
||||
|
||||
const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState(
|
||||
step?.action === WorkflowActions.EMAIL_ADDRESS ? true : false
|
||||
);
|
||||
@@ -116,9 +123,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||
const refReminderBody = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const [numberVerified, setNumberVerified] = useState(
|
||||
verifiedNumbers && step
|
||||
? !!verifiedNumbers.find((number) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`))
|
||||
: false
|
||||
step &&
|
||||
!!verifiedNumbers.find((number) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`))
|
||||
);
|
||||
|
||||
const addVariableBody = (variable: string) => {
|
||||
@@ -451,11 +457,18 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||
verifyPhoneNumberMutation.mutate({
|
||||
phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
|
||||
code: verificationCode,
|
||||
teamId,
|
||||
});
|
||||
}}>
|
||||
Verify
|
||||
{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>
|
||||
|
||||
@@ -54,6 +54,7 @@ export const scheduleWorkflowReminders = async (
|
||||
step.template,
|
||||
step.sender || SENDER_ID,
|
||||
workflow.userId,
|
||||
workflow.teamId,
|
||||
step.numberVerificationPending
|
||||
);
|
||||
} else if (
|
||||
@@ -124,6 +125,7 @@ export const sendCancelledReminders = async (
|
||||
step.template,
|
||||
step.sender || SENDER_ID,
|
||||
workflow.userId,
|
||||
workflow.teamId,
|
||||
step.numberVerificationPending
|
||||
);
|
||||
} else if (
|
||||
|
||||
@@ -53,7 +53,8 @@ export const scheduleSMSReminder = async (
|
||||
workflowStepId: number,
|
||||
template: WorkflowTemplates,
|
||||
sender: string,
|
||||
userId: number,
|
||||
userId?: number | null,
|
||||
teamId?: number | null,
|
||||
isVerificationPending = false
|
||||
) => {
|
||||
const { startTime, endTime } = evt;
|
||||
@@ -69,7 +70,10 @@ export const scheduleSMSReminder = async (
|
||||
async function getIsNumberVerified() {
|
||||
if (action === WorkflowActions.SMS_ATTENDEE) return true;
|
||||
const verifiedNumber = await prisma.verifiedNumber.findFirst({
|
||||
where: { userId, phoneNumber: reminderPhone || "" },
|
||||
where: {
|
||||
OR: [{ userId }, { teamId }],
|
||||
phoneNumber: reminderPhone || "",
|
||||
},
|
||||
});
|
||||
if (!!verifiedNumber) return true;
|
||||
return isVerificationPending;
|
||||
|
||||
@@ -6,13 +6,21 @@ export const sendVerificationCode = async (phoneNumber: string) => {
|
||||
return twilio.sendVerificationCode(phoneNumber);
|
||||
};
|
||||
|
||||
export const verifyPhoneNumber = async (phoneNumber: string, code: string, userId: number) => {
|
||||
export const verifyPhoneNumber = async (
|
||||
phoneNumber: string,
|
||||
code: string,
|
||||
userId?: number,
|
||||
teamId?: number
|
||||
) => {
|
||||
if (!userId && !teamId) return true;
|
||||
|
||||
const verificationStatus = await twilio.verifyNumber(phoneNumber, code);
|
||||
|
||||
if (verificationStatus === "approved") {
|
||||
await prisma.verifiedNumber.create({
|
||||
data: {
|
||||
userId,
|
||||
teamId,
|
||||
phoneNumber,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
import { FiPlus } from "@calcom/ui/components/icon";
|
||||
import { AnimatedPopover, Avatar, CreateButton, showToast } from "@calcom/ui";
|
||||
|
||||
import LicenseRequired from "../../common/components/v2/LicenseRequired";
|
||||
import SkeletonLoader from "../components/SkeletonLoaderList";
|
||||
import type { WorkflowType } from "../components/WorkflowListPage";
|
||||
import WorkflowList from "../components/WorkflowListPage";
|
||||
|
||||
function WorkflowsPage() {
|
||||
const { t } = useLocale();
|
||||
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
const [checkedFilterItems, setCheckedFilterItems] = useState<{ userId: number | null; teamIds: number[] }>({
|
||||
userId: session.data?.user.id || null,
|
||||
teamIds: [],
|
||||
});
|
||||
|
||||
const { data, isLoading } = trpc.viewer.workflows.list.useQuery();
|
||||
const { data: allWorkflowsData, isLoading } = trpc.viewer.workflows.list.useQuery();
|
||||
|
||||
const createMutation = trpc.viewer.workflows.createV2.useMutation({
|
||||
const [filteredWorkflows, setFilteredWorkflows] = useState<WorkflowType[]>([]);
|
||||
|
||||
const createMutation = trpc.viewer.workflows.create.useMutation({
|
||||
onSuccess: async ({ workflow }) => {
|
||||
await router.replace("/workflows/" + workflow.id);
|
||||
},
|
||||
@@ -37,20 +45,66 @@ function WorkflowsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const query = trpc.viewer.workflows.getByViewer.useQuery();
|
||||
|
||||
useEffect(() => {
|
||||
const allWorkflows = allWorkflowsData?.workflows;
|
||||
if (allWorkflows && allWorkflows.length > 0) {
|
||||
const filtered = allWorkflows.filter((workflow) => {
|
||||
if (checkedFilterItems.teamIds.includes(workflow.teamId || 0)) return workflow;
|
||||
if (!workflow.teamId) {
|
||||
if (!!workflow.userId && workflow.userId === checkedFilterItems.userId) return workflow;
|
||||
}
|
||||
});
|
||||
setFilteredWorkflows(filtered);
|
||||
} else {
|
||||
setFilteredWorkflows([]);
|
||||
}
|
||||
}, [checkedFilterItems, allWorkflowsData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (session.status !== "loading" && !query.isLoading) {
|
||||
if (!query.data) return;
|
||||
setCheckedFilterItems({
|
||||
userId: session.data?.user.id || null,
|
||||
teamIds: query.data.profiles
|
||||
.map((profile) => {
|
||||
if (!!profile.teamId) {
|
||||
return profile.teamId;
|
||||
}
|
||||
})
|
||||
.filter((teamId): teamId is number => !!teamId),
|
||||
});
|
||||
}
|
||||
}, [session.status, query.isLoading, allWorkflowsData]);
|
||||
|
||||
if (!query.data) return null;
|
||||
|
||||
const profileOptions = query.data.profiles
|
||||
.filter((profile) => !profile.readOnly)
|
||||
.map((profile) => {
|
||||
return { teamId: profile.teamId, label: profile.name || profile.slug, image: profile.image };
|
||||
});
|
||||
|
||||
return (
|
||||
<Shell
|
||||
heading={t("workflows")}
|
||||
title={t("workflows")}
|
||||
subtitle={t("workflows_to_automate_notifications")}
|
||||
CTA={
|
||||
session.data?.hasValidLicense && data?.workflows && data?.workflows.length > 0 ? (
|
||||
<Button
|
||||
variant="fab"
|
||||
StartIcon={FiPlus}
|
||||
onClick={() => createMutation.mutate()}
|
||||
loading={createMutation.isLoading}>
|
||||
{t("new")}
|
||||
</Button>
|
||||
query.data.profiles.length === 1 &&
|
||||
session.data?.hasValidLicense &&
|
||||
allWorkflowsData?.workflows &&
|
||||
allWorkflowsData?.workflows.length > 0 ? (
|
||||
<CreateButton
|
||||
subtitle={t("new_workflow_subtitle").toUpperCase()}
|
||||
options={profileOptions}
|
||||
createFunction={(teamId?: number) => {
|
||||
createMutation.mutate({ teamId });
|
||||
}}
|
||||
isLoading={createMutation.isLoading}
|
||||
disableMobileButton={true}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
@@ -60,7 +114,31 @@ function WorkflowsPage() {
|
||||
<SkeletonLoader />
|
||||
) : (
|
||||
<>
|
||||
<WorkflowList workflows={data?.workflows} />
|
||||
{query.data.profiles.length > 1 &&
|
||||
allWorkflowsData?.workflows &&
|
||||
allWorkflowsData.workflows.length > 0 && (
|
||||
<div className="mb-4 flex">
|
||||
<Filter
|
||||
profiles={query.data.profiles}
|
||||
checked={checkedFilterItems}
|
||||
setChecked={setCheckedFilterItems}
|
||||
/>
|
||||
<div className="ml-auto">
|
||||
<CreateButton
|
||||
subtitle={t("new_workflow_subtitle").toUpperCase()}
|
||||
options={profileOptions}
|
||||
createFunction={(teamId?: number) => createMutation.mutate({ teamId })}
|
||||
isLoading={createMutation.isLoading}
|
||||
disableMobileButton={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<WorkflowList
|
||||
workflows={filteredWorkflows}
|
||||
profileOptions={profileOptions}
|
||||
hasNoWorkflows={!allWorkflowsData?.workflows || allWorkflowsData?.workflows.length === 0}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</LicenseRequired>
|
||||
@@ -68,4 +146,126 @@ function WorkflowsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const Filter = (props: {
|
||||
profiles: {
|
||||
readOnly?: boolean | undefined;
|
||||
slug: string | null;
|
||||
name: string | null;
|
||||
teamId: number | null | undefined;
|
||||
}[];
|
||||
checked: {
|
||||
userId: number | null;
|
||||
teamIds: number[];
|
||||
};
|
||||
setChecked: Dispatch<
|
||||
SetStateAction<{
|
||||
userId: number | null;
|
||||
teamIds: number[];
|
||||
}>
|
||||
>;
|
||||
}) => {
|
||||
const session = useSession();
|
||||
const userId = session.data?.user.id || 0;
|
||||
const userName = session.data?.user.name || "";
|
||||
|
||||
const teams = props.profiles.filter((profile) => !!profile.teamId);
|
||||
const { checked, setChecked } = props;
|
||||
|
||||
const [noFilter, setNoFilter] = useState(true);
|
||||
|
||||
return (
|
||||
<div className={classNames("-mb-2", noFilter ? "w-16" : "w-[100px]")}>
|
||||
<AnimatedPopover text={noFilter ? "All" : "Filtered"}>
|
||||
<div className="item-center flex px-4 py-[6px] focus-within:bg-gray-100 hover:cursor-pointer hover:bg-gray-50">
|
||||
<Avatar
|
||||
imageSrc=""
|
||||
size="sm"
|
||||
alt={`${userName} Avatar`}
|
||||
gravatarFallbackMd5="fallback"
|
||||
className="self-center"
|
||||
asChild
|
||||
/>
|
||||
<label
|
||||
htmlFor="yourWorkflows"
|
||||
className="ml-2 mr-auto self-center truncate text-sm font-medium text-gray-700">
|
||||
{userName}
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="yourWorkflows"
|
||||
type="checkbox"
|
||||
className="text-primary-600 focus:ring-primary-500 inline-flex h-4 w-4 place-self-center justify-self-end rounded border-gray-300 "
|
||||
checked={!!checked.userId}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setChecked({ userId: userId, teamIds: checked.teamIds });
|
||||
if (checked.teamIds.length === teams.length) {
|
||||
setNoFilter(true);
|
||||
}
|
||||
} else if (!e.target.checked) {
|
||||
setChecked({ userId: null, teamIds: checked.teamIds });
|
||||
|
||||
setNoFilter(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{teams.map((profile) => (
|
||||
<div
|
||||
className="item-center flex px-4 py-[6px] focus-within:bg-gray-100 hover:cursor-pointer hover:bg-gray-50"
|
||||
key={`${profile.teamId || 0}`}>
|
||||
<Avatar
|
||||
imageSrc=""
|
||||
size="sm"
|
||||
alt={`${profile.slug} Avatar`}
|
||||
gravatarFallbackMd5="fallback"
|
||||
className="self-center"
|
||||
asChild
|
||||
/>
|
||||
<label
|
||||
htmlFor={profile.slug || ""}
|
||||
className="ml-2 mr-auto select-none self-center truncate text-sm font-medium text-gray-700 hover:cursor-pointer">
|
||||
{profile.slug}
|
||||
</label>
|
||||
|
||||
<input
|
||||
id={profile.slug || ""}
|
||||
name={profile.slug || ""}
|
||||
type="checkbox"
|
||||
checked={checked.teamIds?.includes(profile.teamId || 0)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
const updatedChecked = checked;
|
||||
updatedChecked.teamIds.push(profile.teamId || 0);
|
||||
setChecked({ userId: checked.userId, teamIds: [...updatedChecked.teamIds] });
|
||||
|
||||
if (checked.userId && updatedChecked.teamIds.length === teams.length) {
|
||||
setNoFilter(true);
|
||||
} else {
|
||||
setNoFilter(false);
|
||||
}
|
||||
} else if (!e.target.checked) {
|
||||
const index = checked.teamIds.indexOf(profile.teamId || 0);
|
||||
if (index !== -1) {
|
||||
const updatedChecked = checked;
|
||||
updatedChecked.teamIds.splice(index, 1);
|
||||
setChecked({ userId: checked.userId, teamIds: [...updatedChecked.teamIds] });
|
||||
|
||||
if (checked.userId && updatedChecked.teamIds.length === teams.length) {
|
||||
setNoFilter(true);
|
||||
} else {
|
||||
setNoFilter(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="text-primary-600 focus:ring-primary-500 inline-flex h-4 w-4 place-self-center justify-self-end rounded border-gray-300 "
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</AnimatedPopover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowsPage;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { WorkflowStep } from "@prisma/client";
|
||||
import {
|
||||
TimeUnit,
|
||||
WorkflowActions,
|
||||
WorkflowStep,
|
||||
WorkflowTemplates,
|
||||
WorkflowTriggerEvents,
|
||||
MembershipRole,
|
||||
} from "@prisma/client";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import { useSession } from "next-auth/react";
|
||||
@@ -21,7 +22,7 @@ import { HttpError } from "@calcom/lib/http-error";
|
||||
import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui";
|
||||
import { Alert, Button, Form, showToast } from "@calcom/ui";
|
||||
import { Alert, Button, Form, showToast, Badge } from "@calcom/ui";
|
||||
|
||||
import LicenseRequired from "../../common/components/v2/LicenseRequired";
|
||||
import SkeletonLoader from "../components/SkeletonLoaderEdit";
|
||||
@@ -87,6 +88,7 @@ function WorkflowPage() {
|
||||
|
||||
const [selectedEventTypes, setSelectedEventTypes] = useState<Option[]>([]);
|
||||
const [isAllDataLoaded, setIsAllDataLoaded] = useState(false);
|
||||
const [isMixedEventType, setIsMixedEventType] = useState(false); //for old event types before team workflows existed
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
mode: "onBlur",
|
||||
@@ -108,10 +110,22 @@ function WorkflowPage() {
|
||||
}
|
||||
);
|
||||
|
||||
const { data: verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery();
|
||||
const { data: verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery(
|
||||
{ teamId: workflow?.team?.id },
|
||||
{
|
||||
enabled: !!workflow?.id,
|
||||
}
|
||||
);
|
||||
|
||||
const readOnly =
|
||||
workflow?.team?.members?.find((member) => member.userId === session.data?.user.id)?.role ===
|
||||
MembershipRole.MEMBER;
|
||||
|
||||
useEffect(() => {
|
||||
if (workflow && !isLoading) {
|
||||
if (workflow.userId && workflow.activeOn.find((active) => !!active.eventType.teamId)) {
|
||||
setIsMixedEventType(true);
|
||||
}
|
||||
setSelectedEventTypes(
|
||||
workflow.activeOn.map((active) => ({
|
||||
value: String(active.eventType.id),
|
||||
@@ -247,14 +261,23 @@ function WorkflowPage() {
|
||||
title={workflow && workflow.name ? workflow.name : "Untitled"}
|
||||
CTA={
|
||||
<div>
|
||||
<Button type="submit">{t("save")}</Button>
|
||||
<Button type="submit" disabled={readOnly}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
heading={
|
||||
session.data?.hasValidLicense &&
|
||||
isAllDataLoaded && (
|
||||
<div className={classNames(workflow && !workflow.name ? "text-gray-400" : "")}>
|
||||
{workflow && workflow.name ? workflow.name : "untitled"}
|
||||
<div className="flex">
|
||||
<div className={classNames(workflow && !workflow.name ? "text-gray-400" : "")}>
|
||||
{workflow && workflow.name ? workflow.name : "untitled"}
|
||||
</div>
|
||||
{workflow && workflow.team && (
|
||||
<Badge className="mt-1 ml-4" variant="gray">
|
||||
{workflow.team.slug}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}>
|
||||
@@ -268,6 +291,8 @@ function WorkflowPage() {
|
||||
workflowId={+workflowId}
|
||||
selectedEventTypes={selectedEventTypes}
|
||||
setSelectedEventTypes={setSelectedEventTypes}
|
||||
teamId={workflow ? workflow.teamId || undefined : undefined}
|
||||
isMixedEventType={isMixedEventType}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user