Enabling workflows for managed events (#8446)
* Enabling workflows for managed events * Disabling fields for workflow when readonly * Disabling event type workflows if readonly * Installing auth app * Reverting yarn.lock * Again reverting yarn * Showing user workflows in managed event type * Reusing existing code to show all workflows * Further extending workflow support * Added unit test * Fixing workflow assignment * Fixing locked workflows for children MET * Update packages/features/ee/workflows/components/EventWorkflowsTab.tsx Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> * Personal workflows fix, matching count numbers * Fixing menu items for managed event types * Fixing type checks * Fixing empty activeOn select * Fixing unit test * handling active workflows for MET * Fixing MET slug nit * Embed option in action restored for non-MET * Correcting mobile view when hidden * More adjustments * fix sms reminder field for children * remove console.log --------- Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
parent
372cd94d9f
commit
cb2225259c
|
@ -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", {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -72,7 +72,7 @@ const CalendarSwitch = (props: ICalendarSwitchProps) => {
|
|||
}
|
||||
);
|
||||
return (
|
||||
<div className={classNames("flex flex-row items-center my-2")}>
|
||||
<div className={classNames("my-2 flex flex-row items-center")}>
|
||||
<div className="flex pl-2">
|
||||
<Switch
|
||||
id={externalId}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 ?? 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 (
|
||||
<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>
|
||||
|
|
|
@ -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 })}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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");
|
|
@ -713,6 +713,7 @@ model WorkflowsOnEventTypes {
|
|||
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
|
||||
eventTypeId Int
|
||||
|
||||
@@unique([workflowId, eventTypeId])
|
||||
@@index([workflowId])
|
||||
@@index([eventTypeId])
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
|
|||
users: true,
|
||||
},
|
||||
},
|
||||
parentId: true,
|
||||
hosts: {
|
||||
select: {
|
||||
user: {
|
||||
|
|
|
@ -248,6 +248,11 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||
userId: true,
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
select: {
|
||||
workflowId: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in New Issue
Block a user