From 97fadcbc8bad364b0cdfd07de4918503384dffae Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Wed, 18 Jan 2023 15:52:49 -0500 Subject: [PATCH 01/64] add teamId to workflows and add first basic query --- .../features/ee/workflows/pages/index.tsx | 2 + packages/prisma/schema.prisma | 7 +- .../trpc/server/routers/viewer/workflows.tsx | 105 +++++++++++++++++- 3 files changed, 111 insertions(+), 3 deletions(-) diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index cc4c2ab0850f50..d010a2c045e0cb 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -36,6 +36,8 @@ function WorkflowsPage() { }, }); + const query = trpc.viewer.workflows.getByViewer.useQuery(); + return ( { + const { prisma } = ctx; + + const user = await prisma.user.findUnique({ + where: { + id: ctx.user.id, + }, + select: { + id: true, + username: true, + name: true, + startTime: true, + endTime: true, + bufferTime: true, + workflows: { + select: { + id: true, + name: true, + }, + }, + teams: { + where: { + accepted: true, + }, + select: { + role: true, + team: { + select: { + id: true, + name: true, + slug: true, + members: { + select: { + userId: true, + }, + }, + workflows: { + select: { + id: true, + name: true, + }, + }, + }, + }, + }, + }, + }, + }); + + if (!user) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); + } + + const userWorkflows = user.workflows; + + type WorkflowGroup = { + teamId?: number | null; + profile: { + slug: typeof user["username"]; + name: typeof user["name"]; + }; + metadata?: { + membershipCount: number; + readOnly: boolean; + }; + workflows: typeof userWorkflows; + }; + + let workflowGroups: WorkflowGroup[] = []; + + workflowGroups.push({ + teamId: null, + profile: { + slug: user.username, + name: user.name, + }, + workflows: userWorkflows, + metadata: { + membershipCount: 1, + readOnly: false, + }, + }); + + workflowGroups = ([] as WorkflowGroup[]).concat( + workflowGroups, + user.teams.map((membership) => ({ + teamId: membership.team.id, + profile: { + name: membership.team.name, + slug: "team/" + membership.team.slug, + }, + metadata: { + membershipCount: membership.team.members.length, + readOnly: membership.role === MembershipRole.MEMBER, + }, + workflows: membership.team.workflows, + })) + ); + + console.log("test test "); + console.log(JSON.stringify(workflowGroups)); + }), }); From 2b247c5fcac41fb31d93c71e1738faba85dd155d Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Mon, 23 Jan 2023 09:32:58 -0500 Subject: [PATCH 02/64] create first version of new button (without any functionality) --- apps/web/public/static/locales/en/common.json | 3 +- .../components/CreateNewWorkflowButton.tsx | 407 ++++++++++++++++++ .../features/ee/workflows/pages/index.tsx | 17 +- .../trpc/server/routers/viewer/workflows.tsx | 10 +- 4 files changed, 427 insertions(+), 10 deletions(-) create mode 100644 packages/features/ee/workflows/components/CreateNewWorkflowButton.tsx diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 8715a036857293..a620f453b643a2 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1512,5 +1512,6 @@ "install_google_meet": "Install Google Meet", "install_google_calendar": "Install Google Calendar", "sender_name": "Sender name", - "no_recordings_found": "No recordings found" + "no_recordings_found": "No recordings found", + "new_workflow_subtitle": "Create a workflow under your account or a team" } diff --git a/packages/features/ee/workflows/components/CreateNewWorkflowButton.tsx b/packages/features/ee/workflows/components/CreateNewWorkflowButton.tsx new file mode 100644 index 00000000000000..7bd80bf2a34892 --- /dev/null +++ b/packages/features/ee/workflows/components/CreateNewWorkflowButton.tsx @@ -0,0 +1,407 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { SchedulingType } from "@prisma/client"; +import { isValidPhoneNumber } from "libphonenumber-js"; +import { useSession } from "next-auth/react"; +import { useRouter } from "next/router"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import classNames from "@calcom/lib/classNames"; +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; +import { HttpError } from "@calcom/lib/http-error"; +import slugify from "@calcom/lib/slugify"; +import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype"; +import { trpc } from "@calcom/trpc/react"; +import { + Alert, + Avatar, + Button, + Dialog, + DialogClose, + DialogContent, + Dropdown, + DropdownMenuContent, + DropdownMenuItem, + DropdownItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, + Form, + Icon, + RadioGroup as RadioArea, + showToast, + TextAreaField, + TextField, +} from "@calcom/ui"; + +// this describes the uniform data needed to create a new event type on Profile or Team +export interface EventTypeParent { + teamId: number | null | undefined; // if undefined, then it's a profile + name?: string | null; + slug?: string | null; + image?: string | null; +} + +interface CreateEventTypeBtnProps { + // set true for use on the team settings page + canAddWorkflows: boolean; + // set true when in use on the team settings page + isIndividualTeam?: boolean; + // EventTypeParent can be a profile (as first option) or a team for the rest. + options: EventTypeParent[]; +} + +const locationFormSchema = z.array( + z.object({ + locationType: z.string(), + locationAddress: z.string().optional(), + displayLocationPublicly: z.boolean().optional(), + locationPhoneNumber: z + .string() + .refine((val) => isValidPhoneNumber(val)) + .optional(), + locationLink: z.string().url().optional(), // URL validates as new URL() - which requires HTTPS:// In the input field + }) +); + +const querySchema = z.object({ + eventPage: z.string(), + teamId: z.union([z.string().transform((val) => +val), z.number()]).optional(), + title: z.string().optional(), + slug: z.string().optional(), + length: z + .union([z.string().transform((val) => +val), z.number()]) + .optional() + .default(15), + description: z.string().optional(), + schedulingType: z.nativeEnum(SchedulingType).optional(), + locations: z + .string() + .transform((jsonString) => locationFormSchema.parse(JSON.parse(jsonString))) + .optional(), +}); + +const CreateEventTypeDialog = () => { + const { t } = useLocale(); + const router = useRouter(); + + const { + data: { teamId, eventPage: pageSlug, ...defaultValues }, + } = useTypedQuery(querySchema); + + const form = useForm>({ + resolver: zodResolver(createEventTypeInput), + defaultValues, + }); + + const { register } = form; + + const createMutation = trpc.viewer.eventTypes.create.useMutation({ + onSuccess: async ({ eventType }) => { + await router.replace("/event-types/" + eventType.id); + showToast(t("event_type_created_successfully", { eventTypeTitle: eventType.title }), "success"); + }, + onError: (err) => { + if (err instanceof HttpError) { + const message = `${err.statusCode}: ${err.message}`; + showToast(message, "error"); + } + + if (err.data?.code === "BAD_REQUEST") { + const message = `${err.data.code}: URL already exists.`; + showToast(message, "error"); + } + + if (err.data?.code === "UNAUTHORIZED") { + const message = `${err.data.code}: You are not able to create this event`; + showToast(message, "error"); + } + }, + }); + + return ( + + +
{ + createMutation.mutate(values); + }}> +
+ {teamId && ( + + )} + { + form.setValue("title", e?.target.value); + if (form.formState.touchedFields["slug"] === undefined) { + form.setValue("slug", slugify(e?.target.value)); + } + }} + /> + + {process.env.NEXT_PUBLIC_WEBSITE_URL !== undefined && + process.env.NEXT_PUBLIC_WEBSITE_URL?.length >= 21 ? ( + /{pageSlug}/} + {...register("slug")} + onChange={(e) => { + form.setValue("slug", slugify(e?.target.value), { shouldTouch: true }); + }} + /> + ) : ( + + {process.env.NEXT_PUBLIC_WEBSITE_URL}/{pageSlug}/ + + } + {...register("slug")} + /> + )} + + + +
+ +
+ + {teamId && ( +
+ + {form.formState.errors.schedulingType && ( + + )} + + + {t("collective")} +

{t("collective_description")}

+
+ + {t("round_robin")} +

{t("round_robin_description")}

+
+
+
+ )} +
+
+ + +
+
+
+
+ ); +}; + +export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) { + const { t } = useLocale(); + const router = useRouter(); + + const hasTeams = !!props.options.find((option) => option.teamId); + + // inject selection data into url for correct router history + // const openModal = (option: EventTypeParent) => { + // const query = { + // ...router.query, + // dialog: "new-eventtype", + // eventPage: option.slug, + // teamId: option.teamId, + // }; + // if (!option.teamId) { + // delete query.teamId; + // } + // router.push( + // { + // pathname: router.pathname, + // query, + // }, + // undefined, + // { shallow: true } + // ); + // }; + + return ( + <> + {!hasTeams || props.isIndividualTeam ? ( + + ) : ( + + + + + + +
{t("new_workflow_subtitle")}
+
+ + {props.options.map((option) => ( + + ( + + )} + onClick={() => console.log("create event type")}> + {option.name ? option.name : option.slug} + + + ))} +
+
+ )} + {/* Dialog for duplicate event type */} + {/* {router.query.dialog === "duplicate-event-type" && } */} + {router.query.dialog === "new-eventtype" && } + + ); +} + +type CreateEventTypeTrigger = { + isIndividualTeam?: boolean; + // EventTypeParent can be a profile (as first option) or a team for the rest. + options: EventTypeParent[]; + hasTeams: boolean; + // set true for use on the team settings page + canAddEvents: boolean; + openModal: (option: EventTypeParent) => void; +}; + +export function CreateEventTypeTrigger(props: CreateEventTypeTrigger) { + const { t } = useLocale(); + + return ( + <> + {!props.hasTeams || props.isIndividualTeam ? ( + + ) : ( + + + + + + {t("new_event_subtitle")} + + {props.options.map((option) => ( + props.openModal(option)} + /> + ))} + + + )} + + ); +} + +function CreateEventTeamsItem(props: { + openModal: (option: EventTypeParent) => void; + option: EventTypeParent; +}) { + const session = useSession(); + const membershipQuery = trpc.viewer.teams.getMembershipbyUser.useQuery({ + memberId: session.data?.user?.id as number, + teamId: props.option.teamId as number, + }); + + const isDisabled = membershipQuery.data?.role === "MEMBER"; + + return ( + props.openModal(props.option)}> + + {props.option.name ? props.option.name : props.option.slug} + + ); +} diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index d010a2c045e0cb..0ba144f1e8640d 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -8,6 +8,7 @@ import { trpc } from "@calcom/trpc/react"; import { Button, Icon, showToast } from "@calcom/ui"; import LicenseRequired from "../../common/components/v2/LicenseRequired"; +import CreateNewWorkflowButton from "../components/CreateNewWorkflowButton"; import SkeletonLoader from "../components/SkeletonLoaderList"; import WorkflowList from "../components/WorkflowListPage"; @@ -37,6 +38,7 @@ function WorkflowsPage() { }); const query = trpc.viewer.workflows.getByViewer.useQuery(); + if (!query.data) return null; return ( 0 ? ( - + // + ) : ( <> ) diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index ca2098aacf8657..eacc5da2874656 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -1210,7 +1210,13 @@ export const workflowsRouter = router({ })) ); - console.log("test test "); - console.log(JSON.stringify(workflowGroups)); + return { + workflowGroups: workflowGroups.filter((groupBy) => !!groupBy.workflows?.length), + profiles: workflowGroups.map((group) => ({ + teamId: group.teamId, + ...group.profile, + ...group.metadata, + })), + }; }), }); From 0cffbd0de4329a149dda8f585a4f22391540a520 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Wed, 25 Jan 2023 13:04:20 -0500 Subject: [PATCH 03/64] create reusable component for CreateButtons --- apps/web/pages/event-types/index.tsx | 20 +- .../components/CreateNewWorkflowButton.tsx | 407 ------------------ .../components/CreateWorkflowDialog.tsx | 11 + .../features/ee/workflows/pages/index.tsx | 12 +- ...peButton.tsx => CreateEventTypeDialog.tsx} | 102 +---- .../eventtypes/components/DuplicateDialog.tsx | 2 +- .../features/eventtypes/components/index.ts | 2 +- .../components/createButton/CreateButton.tsx | 115 +++++ packages/ui/components/createButton/index.ts | 1 + packages/ui/index.tsx | 1 + 10 files changed, 154 insertions(+), 519 deletions(-) delete mode 100644 packages/features/ee/workflows/components/CreateNewWorkflowButton.tsx create mode 100644 packages/features/ee/workflows/components/CreateWorkflowDialog.tsx rename packages/features/eventtypes/components/{CreateEventTypeButton.tsx => CreateEventTypeDialog.tsx} (70%) create mode 100644 packages/ui/components/createButton/CreateButton.tsx create mode 100644 packages/ui/components/createButton/index.ts diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index 674e6d70a40cfc..6e6a11b8fee990 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -4,10 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; import React, { Fragment, useEffect, useState } from "react"; -import { - CreateEventTypeButton, - EventTypeDescriptionLazy as EventTypeDescription, -} from "@calcom/features/eventtypes/components"; +import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components"; +import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog"; import Shell from "@calcom/features/shell/Shell"; import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -31,6 +29,7 @@ import { Avatar, AvatarGroup, Tooltip, + CreateButton, } from "@calcom/ui"; import { FiArrowDown, @@ -195,7 +194,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL const openDuplicateModal = (eventType: EventType, group: EventTypeGroup) => { const query = { ...router.query, - dialog: "duplicate-event-type", + dialog: "duplicate", title: eventType.title, description: eventType.description, slug: eventType.slug, @@ -574,11 +573,20 @@ const CreateFirstEventTypeView = () => { }; const CTA = () => { + const { t } = useLocale(); + const query = trpc.viewer.eventTypes.getByViewer.useQuery(); if (!query.data) return null; - return ; + return ( + + ); }; const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer); diff --git a/packages/features/ee/workflows/components/CreateNewWorkflowButton.tsx b/packages/features/ee/workflows/components/CreateNewWorkflowButton.tsx deleted file mode 100644 index 7bd80bf2a34892..00000000000000 --- a/packages/features/ee/workflows/components/CreateNewWorkflowButton.tsx +++ /dev/null @@ -1,407 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { SchedulingType } from "@prisma/client"; -import { isValidPhoneNumber } from "libphonenumber-js"; -import { useSession } from "next-auth/react"; -import { useRouter } from "next/router"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -import classNames from "@calcom/lib/classNames"; -import { WEBAPP_URL } from "@calcom/lib/constants"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; -import { HttpError } from "@calcom/lib/http-error"; -import slugify from "@calcom/lib/slugify"; -import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype"; -import { trpc } from "@calcom/trpc/react"; -import { - Alert, - Avatar, - Button, - Dialog, - DialogClose, - DialogContent, - Dropdown, - DropdownMenuContent, - DropdownMenuItem, - DropdownItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, - Form, - Icon, - RadioGroup as RadioArea, - showToast, - TextAreaField, - TextField, -} from "@calcom/ui"; - -// this describes the uniform data needed to create a new event type on Profile or Team -export interface EventTypeParent { - teamId: number | null | undefined; // if undefined, then it's a profile - name?: string | null; - slug?: string | null; - image?: string | null; -} - -interface CreateEventTypeBtnProps { - // set true for use on the team settings page - canAddWorkflows: boolean; - // set true when in use on the team settings page - isIndividualTeam?: boolean; - // EventTypeParent can be a profile (as first option) or a team for the rest. - options: EventTypeParent[]; -} - -const locationFormSchema = z.array( - z.object({ - locationType: z.string(), - locationAddress: z.string().optional(), - displayLocationPublicly: z.boolean().optional(), - locationPhoneNumber: z - .string() - .refine((val) => isValidPhoneNumber(val)) - .optional(), - locationLink: z.string().url().optional(), // URL validates as new URL() - which requires HTTPS:// In the input field - }) -); - -const querySchema = z.object({ - eventPage: z.string(), - teamId: z.union([z.string().transform((val) => +val), z.number()]).optional(), - title: z.string().optional(), - slug: z.string().optional(), - length: z - .union([z.string().transform((val) => +val), z.number()]) - .optional() - .default(15), - description: z.string().optional(), - schedulingType: z.nativeEnum(SchedulingType).optional(), - locations: z - .string() - .transform((jsonString) => locationFormSchema.parse(JSON.parse(jsonString))) - .optional(), -}); - -const CreateEventTypeDialog = () => { - const { t } = useLocale(); - const router = useRouter(); - - const { - data: { teamId, eventPage: pageSlug, ...defaultValues }, - } = useTypedQuery(querySchema); - - const form = useForm>({ - resolver: zodResolver(createEventTypeInput), - defaultValues, - }); - - const { register } = form; - - const createMutation = trpc.viewer.eventTypes.create.useMutation({ - onSuccess: async ({ eventType }) => { - await router.replace("/event-types/" + eventType.id); - showToast(t("event_type_created_successfully", { eventTypeTitle: eventType.title }), "success"); - }, - onError: (err) => { - if (err instanceof HttpError) { - const message = `${err.statusCode}: ${err.message}`; - showToast(message, "error"); - } - - if (err.data?.code === "BAD_REQUEST") { - const message = `${err.data.code}: URL already exists.`; - showToast(message, "error"); - } - - if (err.data?.code === "UNAUTHORIZED") { - const message = `${err.data.code}: You are not able to create this event`; - showToast(message, "error"); - } - }, - }); - - return ( - - -
{ - createMutation.mutate(values); - }}> -
- {teamId && ( - - )} - { - form.setValue("title", e?.target.value); - if (form.formState.touchedFields["slug"] === undefined) { - form.setValue("slug", slugify(e?.target.value)); - } - }} - /> - - {process.env.NEXT_PUBLIC_WEBSITE_URL !== undefined && - process.env.NEXT_PUBLIC_WEBSITE_URL?.length >= 21 ? ( - /{pageSlug}/} - {...register("slug")} - onChange={(e) => { - form.setValue("slug", slugify(e?.target.value), { shouldTouch: true }); - }} - /> - ) : ( - - {process.env.NEXT_PUBLIC_WEBSITE_URL}/{pageSlug}/ - - } - {...register("slug")} - /> - )} - - - -
- -
- - {teamId && ( -
- - {form.formState.errors.schedulingType && ( - - )} - - - {t("collective")} -

{t("collective_description")}

-
- - {t("round_robin")} -

{t("round_robin_description")}

-
-
-
- )} -
-
- - -
-
-
-
- ); -}; - -export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) { - const { t } = useLocale(); - const router = useRouter(); - - const hasTeams = !!props.options.find((option) => option.teamId); - - // inject selection data into url for correct router history - // const openModal = (option: EventTypeParent) => { - // const query = { - // ...router.query, - // dialog: "new-eventtype", - // eventPage: option.slug, - // teamId: option.teamId, - // }; - // if (!option.teamId) { - // delete query.teamId; - // } - // router.push( - // { - // pathname: router.pathname, - // query, - // }, - // undefined, - // { shallow: true } - // ); - // }; - - return ( - <> - {!hasTeams || props.isIndividualTeam ? ( - - ) : ( - - - - - - -
{t("new_workflow_subtitle")}
-
- - {props.options.map((option) => ( - - ( - - )} - onClick={() => console.log("create event type")}> - {option.name ? option.name : option.slug} - - - ))} -
-
- )} - {/* Dialog for duplicate event type */} - {/* {router.query.dialog === "duplicate-event-type" && } */} - {router.query.dialog === "new-eventtype" && } - - ); -} - -type CreateEventTypeTrigger = { - isIndividualTeam?: boolean; - // EventTypeParent can be a profile (as first option) or a team for the rest. - options: EventTypeParent[]; - hasTeams: boolean; - // set true for use on the team settings page - canAddEvents: boolean; - openModal: (option: EventTypeParent) => void; -}; - -export function CreateEventTypeTrigger(props: CreateEventTypeTrigger) { - const { t } = useLocale(); - - return ( - <> - {!props.hasTeams || props.isIndividualTeam ? ( - - ) : ( - - - - - - {t("new_event_subtitle")} - - {props.options.map((option) => ( - props.openModal(option)} - /> - ))} - - - )} - - ); -} - -function CreateEventTeamsItem(props: { - openModal: (option: EventTypeParent) => void; - option: EventTypeParent; -}) { - const session = useSession(); - const membershipQuery = trpc.viewer.teams.getMembershipbyUser.useQuery({ - memberId: session.data?.user?.id as number, - teamId: props.option.teamId as number, - }); - - const isDisabled = membershipQuery.data?.role === "MEMBER"; - - return ( - props.openModal(props.option)}> - - {props.option.name ? props.option.name : props.option.slug} - - ); -} diff --git a/packages/features/ee/workflows/components/CreateWorkflowDialog.tsx b/packages/features/ee/workflows/components/CreateWorkflowDialog.tsx new file mode 100644 index 00000000000000..98dddf9b489f0e --- /dev/null +++ b/packages/features/ee/workflows/components/CreateWorkflowDialog.tsx @@ -0,0 +1,11 @@ +import { Dialog, DialogContent } from "@calcom/ui"; + +export const CreateWorkflowDialog = () => { + return ( + + + <>Create Workflow Dialog + + + ); +}; diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index cd7e2213fc8f9a..dd0d9c64fd775e 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -5,11 +5,10 @@ import Shell from "@calcom/features/shell/Shell"; 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 { CreateButton, showToast } from "@calcom/ui"; import LicenseRequired from "../../common/components/v2/LicenseRequired"; -import CreateNewWorkflowButton from "../components/CreateNewWorkflowButton"; +import { CreateWorkflowDialog } from "../components/CreateWorkflowDialog"; import SkeletonLoader from "../components/SkeletonLoaderList"; import WorkflowList from "../components/WorkflowListPage"; @@ -55,7 +54,12 @@ function WorkflowsPage() { // loading={createMutation.isLoading}> // {t("new")} // - + ) : ( <> ) diff --git a/packages/features/eventtypes/components/CreateEventTypeButton.tsx b/packages/features/eventtypes/components/CreateEventTypeDialog.tsx similarity index 70% rename from packages/features/eventtypes/components/CreateEventTypeButton.tsx rename to packages/features/eventtypes/components/CreateEventTypeDialog.tsx index 42afb328fbaf1e..1208a6e787eefb 100644 --- a/packages/features/eventtypes/components/CreateEventTypeButton.tsx +++ b/packages/features/eventtypes/components/CreateEventTypeDialog.tsx @@ -5,7 +5,6 @@ import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { HttpError } from "@calcom/lib/http-error"; @@ -14,27 +13,16 @@ import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype"; import { trpc } from "@calcom/trpc/react"; import { Alert, - Avatar, Button, Dialog, DialogClose, DialogContent, - Dropdown, - DropdownMenuContent, - DropdownMenuItem, - DropdownItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, Form, RadioGroup as RadioArea, showToast, TextAreaField, TextField, } from "@calcom/ui"; -import { FiPlus, FiChevronDown } from "@calcom/ui/components/icon"; - -import { DuplicateDialog } from "./DuplicateDialog"; // this describes the uniform data needed to create a new event type on Profile or Team export interface EventTypeParent { @@ -44,15 +32,6 @@ export interface EventTypeParent { image?: string | null; } -interface CreateEventTypeBtnProps { - // set true for use on the team settings page - canAddEvents: boolean; - // set true when in use on the team settings page - isIndividualTeam?: boolean; - // EventTypeParent can be a profile (as first option) or a team for the rest. - options: EventTypeParent[]; -} - const locationFormSchema = z.array( z.object({ locationType: z.string(), @@ -83,7 +62,7 @@ const querySchema = z.object({ .optional(), }); -const CreateEventTypeDialog = () => { +export default function CreateEventTypeDialog() { const { t } = useLocale(); const router = useRouter(); @@ -123,7 +102,7 @@ const CreateEventTypeDialog = () => { return ( { ); -}; - -export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) { - const { t } = useLocale(); - const router = useRouter(); - - const hasTeams = !!props.options.find((option) => option.teamId); - - // inject selection data into url for correct router history - const openModal = (option: EventTypeParent) => { - const query = { - ...router.query, - dialog: "new-eventtype", - eventPage: option.slug, - teamId: option.teamId, - }; - if (!option.teamId) { - delete query.teamId; - } - router.push( - { - pathname: router.pathname, - query, - }, - undefined, - { shallow: true } - ); - }; - - return ( - <> - {!hasTeams || props.isIndividualTeam ? ( - - ) : ( - - - - - - -
{t("new_event_subtitle")}
-
- - {props.options.map((option) => ( - - ( - - )} - onClick={() => openModal(option)}> - {option.name ? option.name : option.slug} - - - ))} -
-
- )} - {/* Dialog for duplicate event type */} - {router.query.dialog === "duplicate-event-type" && } - {router.query.dialog === "new-eventtype" && } - - ); } diff --git a/packages/features/eventtypes/components/DuplicateDialog.tsx b/packages/features/eventtypes/components/DuplicateDialog.tsx index 9e3082d3eb8516..5c74740c618277 100644 --- a/packages/features/eventtypes/components/DuplicateDialog.tsx +++ b/packages/features/eventtypes/components/DuplicateDialog.tsx @@ -59,7 +59,7 @@ const DuplicateDialog = () => { return (
import("./EventTypeDescription")); diff --git a/packages/ui/components/createButton/CreateButton.tsx b/packages/ui/components/createButton/CreateButton.tsx new file mode 100644 index 00000000000000..3c2fdbf967fd85 --- /dev/null +++ b/packages/ui/components/createButton/CreateButton.tsx @@ -0,0 +1,115 @@ +import { useRouter } from "next/router"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { + Avatar, + Button, + Dropdown, + DropdownItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@calcom/ui"; +import { FiPlus } from "@calcom/ui/components/icon"; + +export interface Parent { + teamId: number | null | undefined; // if undefined, then it's a profile + name?: string | null; + slug?: string | null; + image?: string | null; +} + +interface CreateBtnProps { + // set true for use on the team settings page + canAdd: boolean; + // set true when in use on the team settings page + isIndividualTeam?: boolean; + // EventTypeParent can be a profile (as first option) or a team for the rest. + options: Parent[]; + + createDialog: () => JSX.Element; + dublicateDialog?: () => JSX.Element; + subtitle?: string; +} + +export function CreateButton(props: CreateBtnProps) { + const { t } = useLocale(); + const router = useRouter(); + + const CreateDialog = props.createDialog(); + const DublicateDialog = props.dublicateDialog ? props.dublicateDialog() : null; + + const hasTeams = !!props.options.find((option) => option.teamId); + + // inject selection data into url for correct router history + const openModal = (option: Parent) => { + const query = { + ...router.query, + dialog: "new", + eventPage: option.slug, + teamId: option.teamId, + }; + if (!option.teamId) { + delete query.teamId; + } + router.push( + { + pathname: router.pathname, + query, + }, + undefined, + { shallow: true } + ); + }; + + return ( + <> + {!hasTeams || props.isIndividualTeam ? ( + + ) : ( + + + + + + +
{props.subtitle}
+
+ + {props.options.map((option) => ( + + ( + + )} + onClick={() => openModal(option)}> + {option.name ? option.name : option.slug} + + + ))} +
+
+ )} + {router.query.dialog === "duplicate" && DublicateDialog} + {router.query.dialog === "new" && CreateDialog} + + ); +} diff --git a/packages/ui/components/createButton/index.ts b/packages/ui/components/createButton/index.ts new file mode 100644 index 00000000000000..b73942be771793 --- /dev/null +++ b/packages/ui/components/createButton/index.ts @@ -0,0 +1 @@ +export { CreateButton } from "./CreateButton"; diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index 7f4ab4d00b2fb1..debdadf7166302 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -129,3 +129,4 @@ export { default as MultiSelectCheckboxes } from "./components/form/checkbox/Mul export type { Option as MultiSelectCheckboxesOptionType } from "./components/form/checkbox/MultiSelectCheckboxes"; export { default as ImageUploader } from "./components/image-uploader/ImageUploader"; export type { ButtonColor } from "./components/button/Button"; +export { CreateButton } from "./components/createButton"; From 1921bb30c9f085022fd9473a4b265f428c156ac8 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Wed, 25 Jan 2023 14:05:54 -0500 Subject: [PATCH 04/64] fix dropdown design --- apps/web/public/static/locales/en/common.json | 2 +- packages/features/ee/workflows/pages/index.tsx | 2 +- packages/ui/components/createButton/CreateButton.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index be8eeb7f57e7ca..b2ef5ef1c5d375 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1511,7 +1511,7 @@ "install_google_calendar": "Install Google Calendar", "sender_name": "Sender name", "no_recordings_found": "No recordings found", - "new_workflow_subtitle": "Create a workflow under your account or a team", + "new_workflow_subtitle": "Install on", "reporting": "Reporting", "reporting_feature": "See all incoming from data and download it as a CSV", "teams_plan_required": "Teams plan required", diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index dd0d9c64fd775e..53490a08d8ca9f 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -55,7 +55,7 @@ function WorkflowsPage() { // {t("new")} // -
{props.subtitle}
+
{props.subtitle}
{props.options.map((option) => ( From 46eb2fe4c2d2868d4651d896137e8d09a9b4ee03 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Wed, 25 Jan 2023 15:50:28 -0500 Subject: [PATCH 05/64] continued work on workflow create dialog --- .../components/CreateWorkflowDialog.tsx | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/features/ee/workflows/components/CreateWorkflowDialog.tsx b/packages/features/ee/workflows/components/CreateWorkflowDialog.tsx index 98dddf9b489f0e..a4c48e110bc8b4 100644 --- a/packages/features/ee/workflows/components/CreateWorkflowDialog.tsx +++ b/packages/features/ee/workflows/components/CreateWorkflowDialog.tsx @@ -1,10 +1,41 @@ -import { Dialog, DialogContent } from "@calcom/ui"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Dialog, DialogContent, DialogHeader, Form } from "@calcom/ui"; +import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui"; +import { FiZap } from "@calcom/ui/components/icon"; + +type FormValues = { + name?: string; + activeOn: Option[]; +}; export const CreateWorkflowDialog = () => { + const formSchema = z.object({ + name: z.string(), + activeOn: z.object({ value: z.string(), label: z.string() }).array(), + }); + + const form = useForm({ + resolver: zodResolver(formSchema), + }); + return ( - - <>Create Workflow Dialog + + <> +
+ +
Workflow in Cal.com
+
+ { + console.log("Create Workflow"); + }} + /> +
); From 64bb412e469f8f59bcda7f54f9af38cbff2b844c Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Wed, 25 Jan 2023 15:51:48 -0500 Subject: [PATCH 06/64] add create function and fix wrong unauthorized error --- .../workflows/components/WorkflowListPage.tsx | 2 +- .../features/ee/workflows/pages/index.tsx | 4 +- .../trpc/server/routers/viewer/workflows.tsx | 219 +++++++++--------- .../components/createButton/CreateButton.tsx | 15 +- 4 files changed, 126 insertions(+), 114 deletions(-) diff --git a/packages/features/ee/workflows/components/WorkflowListPage.tsx b/packages/features/ee/workflows/components/WorkflowListPage.tsx index 1827d780c925b0..d26c9321ebbe0b 100644 --- a/packages/features/ee/workflows/components/WorkflowListPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowListPage.tsx @@ -28,7 +28,7 @@ const CreateEmptyWorkflowView = () => { const { t } = useLocale(); const router = useRouter(); - const createMutation = trpc.viewer.workflows.createV2.useMutation({ + const createMutation = trpc.viewer.workflows.create.useMutation({ onSuccess: async ({ workflow }) => { await router.replace("/workflows/" + workflow.id); }, diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index 53490a08d8ca9f..29e96c0c106845 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -20,7 +20,7 @@ function WorkflowsPage() { const { data, isLoading } = trpc.viewer.workflows.list.useQuery(); - const createMutation = trpc.viewer.workflows.createV2.useMutation({ + const createMutation = trpc.viewer.workflows.create.useMutation({ onSuccess: async ({ workflow }) => { await router.replace("/workflows/" + workflow.id); }, @@ -58,7 +58,7 @@ function WorkflowsPage() { subtitle={t("new_workflow_subtitle").toUpperCase()} canAdd={true} options={query.data.profiles} - createDialog={CreateWorkflowDialog} + createFunction={(teamId?: number) => createMutation.mutate({ teamId })} /> ) : ( <> diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index a2dc0a37b1e60e..4d7fe2a74268b6 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -85,12 +85,13 @@ export const workflowsRouter = router({ .query(async ({ ctx, input }) => { const workflow = await ctx.prisma.workflow.findFirst({ where: { - userId: ctx.user.id, id: input.id, }, select: { id: true, name: true, + userId: true, + teamId: true, time: true, timeUnit: true, activeOn: { @@ -106,45 +107,76 @@ export const workflowsRouter = router({ }, }, }); - if (!workflow) { + + let user; + if (workflow && !workflow.userId) { + user = await ctx.prisma.user.findFirst({ + where: { + id: ctx.user.id, + }, + include: { + teams: { + where: { + userId: ctx.user.id, + }, + include: { + team: { + include: { + workflows: true, + }, + }, + }, + }, + }, + }); + } + + if ( + !workflow || + (workflow.userId != ctx.user.id && + !user?.teams.find((membership) => + membership.team.workflows.find((userWorkflow) => userWorkflow.id === input.id) + )) + ) { + // todo make code more readable throw new TRPCError({ code: "UNAUTHORIZED", }); } + return workflow; }), create: authedProcedure .input( z.object({ - name: z.string(), - trigger: z.enum(WORKFLOW_TRIGGER_EVENTS), - action: z.enum(WORKFLOW_ACTIONS), - timeUnit: z.enum(TIME_UNIT).optional(), - time: z.number().optional(), - sendTo: z.string().optional(), + teamId: z.number().optional(), }) ) .mutation(async ({ ctx, input }) => { - const { name, trigger, action, timeUnit, time, sendTo } = input; - const userId = ctx.user.id; + const { teamId } = input; + + const userId = !teamId ? ctx.user.id : undefined; try { const workflow = await ctx.prisma.workflow.create({ data: { - name, - trigger, + name: "", + trigger: WorkflowTriggerEvents.BEFORE_EVENT, + time: 24, + timeUnit: TimeUnit.HOUR, userId, - timeUnit: time ? timeUnit : undefined, - time, + teamId, }, }); await ctx.prisma.workflowStep.create({ data: { stepNumber: 1, - action, + action: WorkflowActions.EMAIL_HOST, + template: WorkflowTemplates.REMINDER, workflowId: workflow.id, - sendTo, + sender: SENDER_NAME, + numberVerificationPending: false, }, }); return { workflow }; @@ -152,35 +184,6 @@ export const workflowsRouter = router({ throw e; } }), - createV2: authedProcedure.mutation(async ({ ctx }) => { - const userId = ctx.user.id; - - try { - const workflow = await ctx.prisma.workflow.create({ - data: { - name: "", - trigger: WorkflowTriggerEvents.BEFORE_EVENT, - time: 24, - timeUnit: TimeUnit.HOUR, - userId, - }, - }); - - await ctx.prisma.workflowStep.create({ - data: { - stepNumber: 1, - action: WorkflowActions.EMAIL_HOST, - template: WorkflowTemplates.REMINDER, - workflowId: workflow.id, - sender: SENDER_NAME, - numberVerificationPending: false, - }, - }); - return { workflow }; - } catch (e) { - throw e; - } - }), delete: authedProcedure .input( z.object({ @@ -904,72 +907,72 @@ export const workflowsRouter = router({ } if (isSMSAction(step.action) /*|| step.action === WorkflowActions.EMAIL_ADDRESS*/ /*) { - const hasTeamPlan = (await ctx.prisma.membership.count({ where: { userId: user.id } })) > 0; - if (!hasTeamPlan) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Team plan needed" }); - } - } +const hasTeamPlan = (await ctx.prisma.membership.count({ where: { userId: user.id } })) > 0; +if (!hasTeamPlan) { +throw new TRPCError({ code: "UNAUTHORIZED", message: "Team plan needed" }); +} +} - const booking = await ctx.prisma.booking.findFirst({ - orderBy: { - createdAt: "desc", - }, - where: { - userId: ctx.user.id, - }, - include: { - attendees: true, - user: true, - }, - }); +const booking = await ctx.prisma.booking.findFirst({ +orderBy: { +createdAt: "desc", +}, +where: { +userId: ctx.user.id, +}, +include: { +attendees: true, +user: true, +}, +}); - let evt: BookingInfo; - if (booking) { - evt = { - uid: booking?.uid, - attendees: - booking?.attendees.map((attendee) => { - return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone }; - }) || [], - organizer: { - language: { - locale: booking?.user?.locale || "", - }, - name: booking?.user?.name || "", - email: booking?.user?.email || "", - timeZone: booking?.user?.timeZone || "", - }, - startTime: booking?.startTime.toISOString() || "", - endTime: booking?.endTime.toISOString() || "", - title: booking?.title || "", - location: booking?.location || null, - additionalNotes: booking?.description || null, - customInputs: booking?.customInputs, - }; - } else { - //if no booking exists create an example booking - evt = { - attendees: [{ name: "John Doe", email: "john.doe@example.com", timeZone: "Europe/London" }], - organizer: { - language: { - locale: ctx.user.locale, - }, - name: ctx.user.name || "", - email: ctx.user.email, - timeZone: ctx.user.timeZone, - }, - startTime: dayjs().add(10, "hour").toISOString(), - endTime: dayjs().add(11, "hour").toISOString(), - title: "Example Booking", - location: "Office", - additionalNotes: "These are additional notes", - }; - } +let evt: BookingInfo; +if (booking) { +evt = { +uid: booking?.uid, +attendees: + booking?.attendees.map((attendee) => { + return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone }; + }) || [], +organizer: { + language: { + locale: booking?.user?.locale || "", + }, + name: booking?.user?.name || "", + email: booking?.user?.email || "", + timeZone: booking?.user?.timeZone || "", +}, +startTime: booking?.startTime.toISOString() || "", +endTime: booking?.endTime.toISOString() || "", +title: booking?.title || "", +location: booking?.location || null, +additionalNotes: booking?.description || null, +customInputs: booking?.customInputs, +}; +} else { +//if no booking exists create an example booking +evt = { +attendees: [{ name: "John Doe", email: "john.doe@example.com", timeZone: "Europe/London" }], +organizer: { + language: { + locale: ctx.user.locale, + }, + name: ctx.user.name || "", + email: ctx.user.email, + timeZone: ctx.user.timeZone, +}, +startTime: dayjs().add(10, "hour").toISOString(), +endTime: dayjs().add(11, "hour").toISOString(), +title: "Example Booking", +location: "Office", +additionalNotes: "These are additional notes", +}; +} - if ( - action === WorkflowActions.EMAIL_ATTENDEE || - action === WorkflowActions.EMAIL_HOST /*|| - action === WorkflowActions.EMAIL_ADDRESS*/ +if ( +action === WorkflowActions.EMAIL_ATTENDEE || +action === WorkflowActions.EMAIL_HOST /*|| +action === WorkflowActions.EMAIL_ADDRESS*/ /*) { scheduleEmailReminder( evt, diff --git a/packages/ui/components/createButton/CreateButton.tsx b/packages/ui/components/createButton/CreateButton.tsx index 1dba0099387f6b..a73056bdfd4f19 100644 --- a/packages/ui/components/createButton/CreateButton.tsx +++ b/packages/ui/components/createButton/CreateButton.tsx @@ -30,8 +30,9 @@ interface CreateBtnProps { // EventTypeParent can be a profile (as first option) or a team for the rest. options: Parent[]; - createDialog: () => JSX.Element; + createDialog?: () => JSX.Element; dublicateDialog?: () => JSX.Element; + createFunction?: (teamId?: number) => void; subtitle?: string; } @@ -39,7 +40,7 @@ export function CreateButton(props: CreateBtnProps) { const { t } = useLocale(); const router = useRouter(); - const CreateDialog = props.createDialog(); + const CreateDialog = props.createDialog ? props.createDialog() : null; const DublicateDialog = props.dublicateDialog ? props.dublicateDialog() : null; const hasTeams = !!props.options.find((option) => option.teamId); @@ -100,7 +101,15 @@ export function CreateButton(props: CreateBtnProps) { {...props} /> )} - onClick={() => openModal(option)}> + onClick={() => + !!CreateDialog + ? openModal(option) + : props.createFunction + ? props.createFunction(option.teamId || undefined) + : null + }> + {" "} + {/*improve this code */} {option.name ? option.name : option.slug} From d605411f40a64875f31faeb409d13936bc0a326b Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Thu, 26 Jan 2023 10:37:56 -0500 Subject: [PATCH 07/64] create new isAuthorized function --- .../trpc/server/routers/viewer/workflows.tsx | 135 +++++++++++------- 1 file changed, 83 insertions(+), 52 deletions(-) diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index 4d7fe2a74268b6..f2998d329bd70d 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -38,6 +38,7 @@ import { SENDER_ID } from "@calcom/lib/constants"; import { SENDER_NAME } from "@calcom/lib/constants"; // import { getErrorFromUnknown } from "@calcom/lib/errors"; import { getTranslation } from "@calcom/lib/server/i18n"; +import { PrismaClient, Workflow } from "@calcom/prisma/client"; import { TRPCError } from "@trpc/server"; @@ -50,6 +51,59 @@ function getSender( return isSMSAction(step.action) ? step.sender || SENDER_ID : step.senderName || SENDER_NAME; } +async function isAuthorized( + workflow: Pick | null, + prisma: PrismaClient< + Prisma.PrismaClientOptions, + never, + Prisma.RejectOnNotFound | Prisma.RejectPerOperation | undefined + >, + currentUserId: number +) { + if (!workflow) { + return false; + } + + const userWorkflow = await prisma.workflow.findFirst({ + where: { + id: workflow.id, + userId: currentUserId, + }, + }); + + if (userWorkflow) return true; + + let user; + + if (!workflow.userId) { + user = await prisma.user.findFirst({ + where: { + id: currentUserId, + }, + include: { + teams: { + include: { + team: { + include: { + workflows: true, + }, + }, + }, + }, + }, + }); + + if ( + !!user?.teams.find((membership) => + membership.team.workflows.find((userWorkflow) => userWorkflow.id === workflow.id) + ) + ) { + return true; + } + } + return false; +} + export const workflowsRouter = router({ list: authedProcedure.query(async ({ ctx }) => { const workflows = await ctx.prisma.workflow.findMany({ @@ -108,37 +162,9 @@ export const workflowsRouter = router({ }, }); - let user; - if (workflow && !workflow.userId) { - user = await ctx.prisma.user.findFirst({ - where: { - id: ctx.user.id, - }, - include: { - teams: { - where: { - userId: ctx.user.id, - }, - include: { - team: { - include: { - workflows: true, - }, - }, - }, - }, - }, - }); - } + const isUserAuthorized = await isAuthorized(workflow, ctx.prisma, ctx.user.id); - if ( - !workflow || - (workflow.userId != ctx.user.id && - !user?.teams.find((membership) => - membership.team.workflows.find((userWorkflow) => userWorkflow.id === input.id) - )) - ) { - // todo make code more readable + if (!isUserAuthorized) { throw new TRPCError({ code: "UNAUTHORIZED", }); @@ -270,7 +296,9 @@ export const workflowsRouter = router({ id, }, select: { + id: true, userId: true, + teamId: true, user: { select: { teams: true, @@ -280,12 +308,15 @@ export const workflowsRouter = router({ }, }); - if ( - !userWorkflow || - userWorkflow.userId !== user.id || - steps.filter((step) => step.workflowId != id).length > 0 - ) + const isUserAuthorized = await isAuthorized(userWorkflow, ctx.prisma, ctx.user.id); + + if (!isUserAuthorized || !userWorkflow) { throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + if (steps.find((step) => step.workflowId != id)) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } const oldActiveOnEventTypes = await ctx.prisma.workflowsOnEventTypes.findMany({ where: { @@ -549,7 +580,7 @@ export const workflowsRouter = router({ //step was edited } else if (JSON.stringify(oldStep) !== JSON.stringify(newStep)) { if ( - !userWorkflow.user.teams.length && + !userWorkflow.user?.teams.length && !isSMSAction(oldStep.action) && isSMSAction(newStep.action) ) { @@ -701,7 +732,7 @@ export const workflowsRouter = router({ //added steps const addedSteps = steps.map((s) => { if (s.id <= 0) { - if (!userWorkflow.user.teams.length && isSMSAction(s.action)) { + if (!userWorkflow.user?.teams.length && isSMSAction(s.action)) { throw new TRPCError({ code: "UNAUTHORIZED" }); } const { id: stepId, ...stepToAdd } = s; @@ -931,16 +962,16 @@ if (booking) { evt = { uid: booking?.uid, attendees: - booking?.attendees.map((attendee) => { - return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone }; - }) || [], +booking?.attendees.map((attendee) => { +return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone }; +}) || [], organizer: { - language: { - locale: booking?.user?.locale || "", - }, - name: booking?.user?.name || "", - email: booking?.user?.email || "", - timeZone: booking?.user?.timeZone || "", +language: { +locale: booking?.user?.locale || "", +}, +name: booking?.user?.name || "", +email: booking?.user?.email || "", +timeZone: booking?.user?.timeZone || "", }, startTime: booking?.startTime.toISOString() || "", endTime: booking?.endTime.toISOString() || "", @@ -954,12 +985,12 @@ customInputs: booking?.customInputs, evt = { attendees: [{ name: "John Doe", email: "john.doe@example.com", timeZone: "Europe/London" }], organizer: { - language: { - locale: ctx.user.locale, - }, - name: ctx.user.name || "", - email: ctx.user.email, - timeZone: ctx.user.timeZone, +language: { +locale: ctx.user.locale, +}, +name: ctx.user.name || "", +email: ctx.user.email, +timeZone: ctx.user.timeZone, }, startTime: dayjs().add(10, "hour").toISOString(), endTime: dayjs().add(11, "hour").toISOString(), From ee6ffcf01027267ab689a1f5b012e5b8b186c5d2 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Thu, 26 Jan 2023 10:56:16 -0500 Subject: [PATCH 08/64] clean up isAuthorized function --- .../trpc/server/routers/viewer/workflows.tsx | 41 ++++++------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index f2998d329bd70d..64c4e1f8e80e0a 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -67,40 +67,23 @@ async function isAuthorized( const userWorkflow = await prisma.workflow.findFirst({ where: { id: workflow.id, - userId: currentUserId, - }, - }); - - if (userWorkflow) return true; - - let user; - - if (!workflow.userId) { - user = await prisma.user.findFirst({ - where: { - id: currentUserId, - }, - include: { - teams: { - include: { - team: { - include: { - workflows: true, + OR: [ + { userId: currentUserId }, + { + team: { + members: { + some: { + userId: currentUserId, }, }, }, }, - }, - }); + ], + }, + }); + + if (userWorkflow) return true; - if ( - !!user?.teams.find((membership) => - membership.team.workflows.find((userWorkflow) => userWorkflow.id === workflow.id) - ) - ) { - return true; - } - } return false; } From ba05926547d76cd7af95c03a3cc52956c883c7bd Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Thu, 26 Jan 2023 16:16:12 -0500 Subject: [PATCH 09/64] create first filter for workflows --- .../ee/workflows/components/DeleteDialog.tsx | 2 +- .../components/EventWorkflowsTab.tsx | 2 +- .../workflows/components/WorkflowListPage.tsx | 2 +- .../features/ee/workflows/pages/index.tsx | 119 +++++++++++++++++- .../trpc/server/routers/viewer/workflows.tsx | 110 ++++++++++++---- 5 files changed, 208 insertions(+), 27 deletions(-) diff --git a/packages/features/ee/workflows/components/DeleteDialog.tsx b/packages/features/ee/workflows/components/DeleteDialog.tsx index 925d7a75ecc14b..8bc9ffd89d15b4 100644 --- a/packages/features/ee/workflows/components/DeleteDialog.tsx +++ b/packages/features/ee/workflows/components/DeleteDialog.tsx @@ -19,7 +19,7 @@ export const DeleteDialog = (props: IDeleteDialog) => { const deleteMutation = trpc.viewer.workflows.delete.useMutation({ onSuccess: async () => { - await utils.viewer.workflows.list.invalidate(); + await utils.viewer.workflows.filteredList.invalidate(); additionalFunction(); showToast(t("workflow_deleted_successfully"), "success"); setIsOpenDialog(false); diff --git a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx index 21a3ef58f70c85..f4181297227047 100644 --- a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx +++ b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx @@ -155,7 +155,7 @@ type Props = { function EventWorkflowsTab(props: Props) { const { workflows } = props; const { t } = useLocale(); - const { data, isLoading } = trpc.viewer.workflows.list.useQuery(); + const { data, isLoading } = trpc.viewer.workflows.filteredList.useQuery(); const router = useRouter(); const [sortedWorkflows, setSortedWorkflows] = useState>([]); diff --git a/packages/features/ee/workflows/components/WorkflowListPage.tsx b/packages/features/ee/workflows/components/WorkflowListPage.tsx index d26c9321ebbe0b..9cd2a6b32bedf2 100644 --- a/packages/features/ee/workflows/components/WorkflowListPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowListPage.tsx @@ -202,7 +202,7 @@ export default function WorkflowListPage({ workflows }: Props) { setIsOpenDialog={setDeleteDialogOpen} workflowId={workflowToDeleteId} additionalFunction={async () => { - await utils.viewer.workflows.list.invalidate(); + await utils.viewer.workflows.filteredList.invalidate(); }} /> diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index 29e96c0c106845..aebd2887750a30 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -1,14 +1,14 @@ import { useSession } from "next-auth/react"; import { useRouter } from "next/router"; +import { useState, Dispatch, SetStateAction } from "react"; import Shell from "@calcom/features/shell/Shell"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; import { trpc } from "@calcom/trpc/react"; -import { CreateButton, showToast } from "@calcom/ui"; +import { AnimatedPopover, Avatar, CreateButton, showToast } from "@calcom/ui"; import LicenseRequired from "../../common/components/v2/LicenseRequired"; -import { CreateWorkflowDialog } from "../components/CreateWorkflowDialog"; import SkeletonLoader from "../components/SkeletonLoaderList"; import WorkflowList from "../components/WorkflowListPage"; @@ -17,8 +17,12 @@ function WorkflowsPage() { const session = useSession(); const router = useRouter(); + const [checkedFilterItems, setCheckedFilterItems] = useState<{ userId: number | null; teamIds: number[] }>({ + userId: null, + teamIds: [], + }); - const { data, isLoading } = trpc.viewer.workflows.list.useQuery(); + const { data, isLoading } = trpc.viewer.workflows.filteredList.useQuery(checkedFilterItems); const createMutation = trpc.viewer.workflows.create.useMutation({ onSuccess: async ({ workflow }) => { @@ -69,6 +73,11 @@ function WorkflowsPage() { ) : ( <> + )} @@ -77,4 +86,108 @@ function WorkflowsPage() { ); } +const Filter = (props: { + profiles: { + membershipCount?: number | undefined; + 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 { t } = useLocale(); + 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 teamNames = teams.map((profile) => profile.name); + const { checked, setChecked } = props; + + return ( + 0 ? `${teamNames.join(", ")}` : ""}> +
+ + + + { + if (e.target.checked) { + setChecked({ userId: userId, teamIds: checked.teamIds }); + } else if (!e.target.checked) { + setChecked({ userId: null, teamIds: checked.teamIds }); + } + }} + /> +
+ {teams.map((profile) => ( +
+ + + + { + if (e.target.checked) { + const updatedChecked = checked; + updatedChecked.teamIds.push(profile.teamId || 0); + setChecked({ userId: checked.userId, teamIds: [...updatedChecked.teamIds] }); + } 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] }); + } + } + }} + className="text-primary-600 focus:ring-primary-500 inline-flex h-4 w-4 place-self-center justify-self-end rounded border-gray-300 " + /> +
+ ))} +
+ ); +}; + export default WorkflowsPage; diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index 64c4e1f8e80e0a..c9045eb0be492e 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -42,7 +42,7 @@ import { PrismaClient, Workflow } from "@calcom/prisma/client"; import { TRPCError } from "@trpc/server"; -import { router, authedProcedure, authedRateLimitedProcedure } from "../../trpc"; +import { router, authedProcedure } from "../../trpc"; import { viewerTeamsRouter } from "./teams"; function getSender( @@ -88,31 +88,99 @@ async function isAuthorized( } export const workflowsRouter = router({ - list: authedProcedure.query(async ({ ctx }) => { - const workflows = await ctx.prisma.workflow.findMany({ - where: { - userId: ctx.user.id, - }, - include: { - activeOn: { - select: { - eventType: { + filteredList: authedProcedure + .input( + z + .object({ + userId: z.number().nullable(), + teamIds: z.number().array().optional(), + }) + .optional() + ) + .query(async ({ ctx, input }) => { + // no filter applied + if (!input || (!input.userId && (!input.teamIds || input.teamIds.length === 0))) { + const workflows = await ctx.prisma.workflow.findMany({ + where: { + OR: [ + { userId: ctx.user.id }, + { + team: { + members: { + some: { + userId: ctx.user.id, + }, + }, + }, + }, + ], + }, + include: { + activeOn: { select: { - id: true, - title: true, + eventType: { + select: { + id: true, + title: true, + }, + }, }, }, + steps: true, }, - }, - steps: true, - }, - orderBy: { - id: "asc", - }, - }); + orderBy: { + id: "asc", + }, + }); - return { workflows }; - }), + return { workflows }; + } + + const { userId, teamIds } = input; + + if (userId && userId != ctx.user.id) { + throw new TRPCError({ + code: "UNAUTHORIZED", + }); + } + const workflows = await ctx.prisma.workflow.findMany({ + where: { + OR: [ + { userId: userId || 0 }, + { + team: { + members: { + some: { + userId: ctx.user.id, + teamId: { + in: teamIds || [], + }, + }, + }, + }, + }, + ], + }, + include: { + activeOn: { + select: { + eventType: { + select: { + id: true, + title: true, + }, + }, + }, + }, + steps: true, + }, + orderBy: { + id: "asc", + }, + }); + console.log(JSON.stringify(workflows)); + return { workflows }; + }), get: authedProcedure .input( z.object({ From c9726b9755a51423414801d02cfa4c340cafcf4c Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Wed, 1 Feb 2023 09:57:29 -0500 Subject: [PATCH 10/64] default check all fitlers --- .../features/ee/workflows/pages/index.tsx | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index aebd2887750a30..16c7a7b6ac7923 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -1,6 +1,6 @@ import { useSession } from "next-auth/react"; import { useRouter } from "next/router"; -import { useState, Dispatch, SetStateAction } from "react"; +import { useState, Dispatch, SetStateAction, useEffect } from "react"; import Shell from "@calcom/features/shell/Shell"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -18,7 +18,7 @@ function WorkflowsPage() { const session = useSession(); const router = useRouter(); const [checkedFilterItems, setCheckedFilterItems] = useState<{ userId: number | null; teamIds: number[] }>({ - userId: null, + userId: session.data?.user.id || null, teamIds: [], }); @@ -42,7 +42,20 @@ function WorkflowsPage() { }); const query = trpc.viewer.workflows.getByViewer.useQuery(); - if (!query.data) return null; + + 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; + } + }) as number[], + }); + } + }, [session.status, query.isLoading]); return ( ) : ( <> - + {data?.workflows && data?.workflows.length > 0 && ( + + )} )} From fc691f7cfcc3f0fd5df4d58adea063236d661bfe Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Wed, 1 Feb 2023 10:20:00 -0500 Subject: [PATCH 11/64] fix empty workflow page --- .../ee/workflows/components/EmptyScreen.tsx | 93 +++++++++++-------- .../workflows/components/WorkflowListPage.tsx | 38 +------- .../components/createButton/CreateButton.tsx | 11 ++- 3 files changed, 60 insertions(+), 82 deletions(-) diff --git a/packages/features/ee/workflows/components/EmptyScreen.tsx b/packages/features/ee/workflows/components/EmptyScreen.tsx index edcdc95caa43a6..9b69ca13e4d283 100644 --- a/packages/features/ee/workflows/components/EmptyScreen.tsx +++ b/packages/features/ee/workflows/components/EmptyScreen.tsx @@ -1,9 +1,11 @@ -import React from "react"; +import { useRouter } from "next/router"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { HttpError } from "@calcom/lib/http-error"; +import { trpc } from "@calcom/trpc/react"; import { SVGComponent } from "@calcom/types/SVGComponent"; -import { Button } from "@calcom/ui"; -import { FiSmartphone, FiMail, FiPlus } from "@calcom/ui/components/icon"; +import { CreateButton, showToast } from "@calcom/ui"; +import { FiSmartphone, FiMail, FiPlus, FiZap } from "@calcom/ui/components/icon"; type WorkflowExampleType = { Icon: SVGComponent; @@ -31,24 +33,27 @@ 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) => void; - isLoading: boolean; - showExampleWorkflows: boolean; -}) { +export default function EmptyScreen() { const { t } = useLocale(); + const utils = trpc.useContext(); + 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 able to create this workflow`; + showToast(message, "error"); + } + }, + }); const workflowsExamples = [ { icon: FiSmartphone, text: t("workflow_example_1") }, @@ -64,34 +69,40 @@ export default function EmptyScreen({ <>
- +
-

{headline}

+

{t("workflows")}

- {description} + {t("no_workflows_description")}

- {buttonOnClick && buttonText && ( - - )} + createMutation.mutate({ teamId })} + buttonText={t("create_workflow")} + /> + { + // + }
- {showExampleWorkflows && ( -
-
- {workflowsExamples.map((example, index) => ( - - ))} -
+
+
+ {workflowsExamples.map((example, index) => ( + + ))}
- )} +
); } diff --git a/packages/features/ee/workflows/components/WorkflowListPage.tsx b/packages/features/ee/workflows/components/WorkflowListPage.tsx index 9cd2a6b32bedf2..f26a7dac068f54 100644 --- a/packages/features/ee/workflows/components/WorkflowListPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowListPage.tsx @@ -5,7 +5,6 @@ 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,7 +14,6 @@ import { DropdownMenuItem, DropdownItem, DropdownMenuTrigger, - showToast, Tooltip, } from "@calcom/ui"; import { FiEdit2, FiLink, FiMoreHorizontal, FiTrash2, FiZap } from "@calcom/ui/components/icon"; @@ -24,40 +22,6 @@ 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.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 able to create this workflow`; - showToast(message, "error"); - } - }, - }); - - return ( - createMutation.mutate()} - IconHeading={FiZap} - headline={t("workflows")} - description={t("no_workflows_description")} - isLoading={createMutation.isLoading} - showExampleWorkflows={true} - /> - ); -}; - export type WorkflowType = Workflow & { steps: WorkflowStep[]; activeOn: { @@ -207,7 +171,7 @@ export default function WorkflowListPage({ workflows }: Props) { />
) : ( - + )} ); diff --git a/packages/ui/components/createButton/CreateButton.tsx b/packages/ui/components/createButton/CreateButton.tsx index a73056bdfd4f19..9fa907197d7841 100644 --- a/packages/ui/components/createButton/CreateButton.tsx +++ b/packages/ui/components/createButton/CreateButton.tsx @@ -34,6 +34,8 @@ interface CreateBtnProps { dublicateDialog?: () => JSX.Element; createFunction?: (teamId?: number) => void; subtitle?: string; + className?: string; + buttonText?: string; } export function CreateButton(props: CreateBtnProps) { @@ -74,14 +76,15 @@ export function CreateButton(props: CreateBtnProps) { data-testid="new-event-type" StartIcon={FiPlus} variant="fab" - disabled={!props.canAdd}> - {t("new")} + disabled={!props.canAdd} + className={props.className}> + {props.buttonText ? props.buttonText : t("new")} ) : ( - From f9ed27e2f05556df3436cde11b0ea15e1dff1c06 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Wed, 1 Feb 2023 11:37:06 -0500 Subject: [PATCH 12/64] filter workflows in frontend --- .../ee/workflows/components/EmptyScreen.tsx | 13 +- .../workflows/components/WorkflowListPage.tsx | 13 +- .../features/ee/workflows/pages/index.tsx | 40 +++++-- .../trpc/server/routers/viewer/workflows.tsx | 113 +++++------------- 4 files changed, 78 insertions(+), 101 deletions(-) diff --git a/packages/features/ee/workflows/components/EmptyScreen.tsx b/packages/features/ee/workflows/components/EmptyScreen.tsx index 9b69ca13e4d283..d55a0948088047 100644 --- a/packages/features/ee/workflows/components/EmptyScreen.tsx +++ b/packages/features/ee/workflows/components/EmptyScreen.tsx @@ -33,9 +33,16 @@ function WorkflowExample(props: WorkflowExampleType) { ); } -export default function EmptyScreen() { +export default function EmptyScreen(props: { + profiles: { + membershipCount?: number | undefined; + readOnly?: boolean | undefined; + slug: string | null; + name: string | null; + teamId: number | null | undefined; + }[]; +}) { const { t } = useLocale(); - const utils = trpc.useContext(); const router = useRouter(); const createMutation = trpc.viewer.workflows.create.useMutation({ @@ -79,7 +86,7 @@ export default function EmptyScreen() { createMutation.mutate({ teamId })} buttonText={t("create_workflow")} diff --git a/packages/features/ee/workflows/components/WorkflowListPage.tsx b/packages/features/ee/workflows/components/WorkflowListPage.tsx index f26a7dac068f54..d307af0f25f6eb 100644 --- a/packages/features/ee/workflows/components/WorkflowListPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowListPage.tsx @@ -33,8 +33,15 @@ export type WorkflowType = Workflow & { }; interface Props { workflows: WorkflowType[] | undefined; + profiles: { + membershipCount?: number | undefined; + readOnly?: boolean | undefined; + slug: string | null; + name: string | null; + teamId: number | null | undefined; + }[]; } -export default function WorkflowListPage({ workflows }: Props) { +export default function WorkflowListPage({ workflows, profiles }: Props) { const { t } = useLocale(); const utils = trpc.useContext(); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); @@ -166,12 +173,12 @@ export default function WorkflowListPage({ workflows }: Props) { setIsOpenDialog={setDeleteDialogOpen} workflowId={workflowToDeleteId} additionalFunction={async () => { - await utils.viewer.workflows.filteredList.invalidate(); + await utils.viewer.workflows.list.invalidate(); }} /> ) : ( - + )} ); diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index 16c7a7b6ac7923..fc9fa1cf0117c6 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -11,10 +11,10 @@ import { AnimatedPopover, Avatar, CreateButton, showToast } from "@calcom/ui"; import LicenseRequired from "../../common/components/v2/LicenseRequired"; import SkeletonLoader from "../components/SkeletonLoaderList"; import WorkflowList from "../components/WorkflowListPage"; +import { Workflow } from ".prisma/client"; function WorkflowsPage() { const { t } = useLocale(); - const session = useSession(); const router = useRouter(); const [checkedFilterItems, setCheckedFilterItems] = useState<{ userId: number | null; teamIds: number[] }>({ @@ -22,7 +22,10 @@ function WorkflowsPage() { teamIds: [], }); - const { data, isLoading } = trpc.viewer.workflows.filteredList.useQuery(checkedFilterItems); + const allWorkflowsQuery = trpc.viewer.workflows.list.useQuery(); + const { data: allWorkflowsData, isLoading } = allWorkflowsQuery; + + const [filteredWorkflows, setFilteredWorkflows] = useState([]); const createMutation = trpc.viewer.workflows.create.useMutation({ onSuccess: async ({ workflow }) => { @@ -43,27 +46,44 @@ 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 (workflow.userId === checkedFilterItems.userId) return workflow; + if (checkedFilterItems.teamIds.includes(workflow.teamId)) return workflow; + }); + setFilteredWorkflows(filtered); + } + }, [checkedFilterItems]); + 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; - } - }) as number[], + teamIds: query.data.profiles + .map((profile) => { + if (!!profile.teamId) { + return profile.teamId; + } + }) + .filter((teamId) => !!teamId) as number[], }); } }, [session.status, query.isLoading]); + if (!query.data) return null; + return ( 0 ? ( + session.data?.hasValidLicense && + allWorkflowsData?.workflows && + allWorkflowsData?.workflows.length > 0 ? ( // - createMutation.mutate({ teamId })} - /> - ) : ( - <> - ) - }> + subtitle={t("workflows_to_automate_notifications")}> {isLoading ? ( ) : ( <> {allWorkflowsData?.workflows && allWorkflowsData.workflows.length > 0 && ( -
+
+
+ createMutation.mutate({ teamId })} + /> +
)} +
{ - if (reminder.referenceId) { - if (reminder.method === WorkflowMethods.EMAIL) { - deleteScheduledEmailReminder(reminder.referenceId); - } else if (reminder.method === WorkflowMethods.SMS) { - deleteScheduledSMSReminder(reminder.referenceId); - } + scheduledReminders.forEach((reminder) => { + if (reminder.referenceId) { + if (reminder.method === WorkflowMethods.EMAIL) { + deleteScheduledEmailReminder(reminder.referenceId); + } else if (reminder.method === WorkflowMethods.SMS) { + deleteScheduledSMSReminder(reminder.referenceId); } - }); + } + }); - await ctx.prisma.workflow.deleteMany({ - where: { - userId: ctx.user.id, - id, - }, - }); - } + await ctx.prisma.workflow.deleteMany({ + where: { + id, + }, + }); return { id, From 89adbba016b49892d58a8798059da15f3bc19e5a Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Thu, 2 Feb 2023 11:13:39 -0500 Subject: [PATCH 20/64] add loading state to create button --- .../features/ee/workflows/components/EmptyScreen.tsx | 11 +---------- packages/features/ee/workflows/pages/index.tsx | 1 + packages/ui/components/createButton/CreateButton.tsx | 3 ++- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/features/ee/workflows/components/EmptyScreen.tsx b/packages/features/ee/workflows/components/EmptyScreen.tsx index be1c18643ab7d6..5d2a75bb77dade 100644 --- a/packages/features/ee/workflows/components/EmptyScreen.tsx +++ b/packages/features/ee/workflows/components/EmptyScreen.tsx @@ -102,17 +102,8 @@ export default function EmptyScreen(props: { className="mx-auto mt-8" createFunction={(teamId?: number) => createMutation.mutate({ teamId })} buttonText={t("create_workflow")} + isLoading={createMutation.isLoading} /> - { - // - }
diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index 8b07c251bbda85..7e80e8ce1e8ea9 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -108,6 +108,7 @@ function WorkflowsPage() { canAdd={true} options={query.data.profiles} createFunction={(teamId?: number) => createMutation.mutate({ teamId })} + isLoading={createMutation.isLoading} />
diff --git a/packages/ui/components/createButton/CreateButton.tsx b/packages/ui/components/createButton/CreateButton.tsx index d6414c3b75e08e..ba39b9db3f5d9d 100644 --- a/packages/ui/components/createButton/CreateButton.tsx +++ b/packages/ui/components/createButton/CreateButton.tsx @@ -36,6 +36,7 @@ interface CreateBtnProps { subtitle?: string; className?: string; buttonText?: string; + isLoading?: boolean; } export function CreateButton(props: CreateBtnProps) { @@ -83,7 +84,7 @@ export function CreateButton(props: CreateBtnProps) { ) : ( - From 1945e5d40950b8884bf69c537487fc8849a3c36c Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Mon, 6 Feb 2023 15:44:15 -0500 Subject: [PATCH 21/64] add badge with team name --- .../workflows/components/WorkflowListPage.tsx | 66 +++++++++++-------- .../trpc/server/routers/viewer/workflows.tsx | 1 + 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/packages/features/ee/workflows/components/WorkflowListPage.tsx b/packages/features/ee/workflows/components/WorkflowListPage.tsx index bd0f0abeca1a5d..1acc2a3ae94882 100644 --- a/packages/features/ee/workflows/components/WorkflowListPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowListPage.tsx @@ -5,6 +5,7 @@ import { useState } from "react"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Team } from "@calcom/prisma/client"; import { trpc } from "@calcom/trpc/react"; import { Button, @@ -15,6 +16,7 @@ import { DropdownItem, DropdownMenuTrigger, Tooltip, + Badge, } from "@calcom/ui"; import { FiEdit2, FiLink, FiMoreHorizontal, FiTrash2, FiZap } from "@calcom/ui/components/icon"; @@ -23,6 +25,7 @@ import { DeleteDialog } from "./DeleteDialog"; import EmptyScreen from "./EmptyScreen"; export type WorkflowType = Workflow & { + team: Team; steps: WorkflowStep[]; activeOn: { eventType: { @@ -75,38 +78,49 @@ export default function WorkflowListPage({ workflows, profiles, hasNoWorkflows } ")" : "Untitled"} -
    -
  • -
    - {getActionIcon(workflow.steps)} +
      +
    • + +
      + {getActionIcon(workflow.steps)} - {t("triggers")} - {workflow.timeUnit && workflow.time && ( - - {t(`${workflow.timeUnit.toLowerCase()}`, { count: workflow.time })} - - )} - {t(`${workflow.trigger.toLowerCase()}_trigger`)} -
      + {t("triggers")} + {workflow.timeUnit && workflow.time && ( + + {t(`${workflow.timeUnit.toLowerCase()}`, { count: workflow.time })} + + )} + {t(`${workflow.trigger.toLowerCase()}_trigger`)} +
    +
  • -
  • - {workflow.activeOn && workflow.activeOn.length > 0 ? ( - ( -

    {activeOn.eventType.title}

    - ))}> +
  • + + {workflow.activeOn && workflow.activeOn.length > 0 ? ( + ( +

    {activeOn.eventType.title}

    + ))}> +
    +
    +
    + ) : (
    - - ) : ( -
    -
    - )} + )} +
  • + {workflow.teamId && ( +
  • + + <>{workflow.team.name} + +
  • + )}
diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index 219788dff9df9b..f90ee8f632325e 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -116,6 +116,7 @@ export const workflowsRouter = router({ }, }, steps: true, + team: true, }, orderBy: { id: "asc", From 8757998786ec111d7aada69efb04784080e804de Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Tue, 7 Feb 2023 12:13:32 -0500 Subject: [PATCH 22/64] handle existing mixed event types --- .../components/WorkflowDetailsPage.tsx | 17 +++++++++++++++-- .../features/ee/workflows/pages/workflow.tsx | 5 +++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx index 6bcb4a6ba59854..961dc00de8e660 100644 --- a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx @@ -22,10 +22,11 @@ interface Props { selectedEventTypes: Option[]; setSelectedEventTypes: Dispatch>; teamId?: number; + isMixedEventType: boolean; } export default function WorkflowDetailsPage(props: Props) { - const { form, workflowId, selectedEventTypes, setSelectedEventTypes, teamId } = props; + const { form, workflowId, selectedEventTypes, setSelectedEventTypes, teamId, isMixedEventType } = props; const { t } = useLocale(); const router = useRouter(); @@ -59,6 +60,18 @@ export default function WorkflowDetailsPage(props: Props) { [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, @@ -111,7 +124,7 @@ export default function WorkflowDetailsPage(props: Props) { render={() => { return ( ([]); const [isAllDataLoaded, setIsAllDataLoaded] = useState(false); + const [isMixedEventType, setIsMixedEventType] = useState(false); //for old event types before teams workflows existed const form = useForm({ mode: "onBlur", @@ -112,6 +113,9 @@ function WorkflowPage() { useEffect(() => { if (workflow && (workflow.steps.length === 0 || workflow.steps[0].stepNumber === 1)) { + if (workflow.userId && workflow.activeOn.find((active) => !!active.eventType.teamId)) { + setIsMixedEventType(true); + } setSelectedEventTypes( workflow.activeOn.map((active) => ({ value: String(active.eventType.id), @@ -276,6 +280,7 @@ function WorkflowPage() { selectedEventTypes={selectedEventTypes} setSelectedEventTypes={setSelectedEventTypes} teamId={workflow ? workflow.teamId || undefined : undefined} + isMixedEventType={isMixedEventType} /> ) : ( From 3e51533a1d91362147206a28dfca58f8077f052f Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Tue, 7 Feb 2023 12:25:37 -0500 Subject: [PATCH 23/64] improve empty screen for filtered view --- .../ee/workflows/components/EmptyScreen.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/features/ee/workflows/components/EmptyScreen.tsx b/packages/features/ee/workflows/components/EmptyScreen.tsx index 5d2a75bb77dade..d48f8f51fdedc1 100644 --- a/packages/features/ee/workflows/components/EmptyScreen.tsx +++ b/packages/features/ee/workflows/components/EmptyScreen.tsx @@ -4,8 +4,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; import { trpc } from "@calcom/trpc/react"; import { SVGComponent } from "@calcom/types/SVGComponent"; -import { CreateButton, showToast } from "@calcom/ui"; -import { FiSmartphone, FiMail, FiPlus, FiZap } from "@calcom/ui/components/icon"; +import { CreateButton, showToast, EmptyScreen as ClassicEmptyScreen } from "@calcom/ui"; +import { FiSmartphone, FiMail, FiZap } from "@calcom/ui/components/icon"; type WorkflowExampleType = { Icon: SVGComponent; @@ -74,14 +74,7 @@ export default function EmptyScreen(props: { // 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 ( -
-

{t("no_workflows")}

-

- {t("change_filter")} -

-
- ); + return ; } return ( From aaee6ae2b8b5edcdb7ed045ed0f2f4d094cf15a1 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Tue, 7 Feb 2023 12:48:18 -0500 Subject: [PATCH 24/64] fix subtitle for create event type --- apps/web/pages/event-types/index.tsx | 2 +- apps/web/public/static/locales/en/common.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index 530929f8678bad..b7051518f831fd 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -622,7 +622,7 @@ const CTA = () => { return ( Date: Tue, 7 Feb 2023 12:53:47 -0500 Subject: [PATCH 25/64] fix position of chevron icon --- packages/ui/components/popover/AnimatedPopover.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/ui/components/popover/AnimatedPopover.tsx b/packages/ui/components/popover/AnimatedPopover.tsx index 6ac46a108f05d3..474c1cf782965d 100644 --- a/packages/ui/components/popover/AnimatedPopover.tsx +++ b/packages/ui/components/popover/AnimatedPopover.tsx @@ -43,9 +43,7 @@ export const AnimatedPopover = ({
+ className="item-center mb-2 flex h-9 max-h-72 justify-between overflow-y-scroll whitespace-nowrap rounded-md border border-gray-300 px-3 py-2 text-sm placeholder:text-gray-400 hover:cursor-pointer hover:border-gray-400 focus:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-800 focus:ring-offset-1">
{text} @@ -54,7 +52,7 @@ export const AnimatedPopover = ({ )}
From be9999d5068127bf4750ae79b1f6803fedec4a08 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Tue, 7 Feb 2023 14:50:19 -0500 Subject: [PATCH 26/64] add new workflow button as cta button of shell --- .../features/ee/workflows/pages/index.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index 7e80e8ce1e8ea9..68d6e0e1cb1e7c 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -89,6 +89,18 @@ function WorkflowsPage() { 0 && ( + createMutation.mutate({ teamId })} + isLoading={createMutation.isLoading} + /> + ) + } subtitle={t("workflows_to_automate_notifications")}> {isLoading ? ( @@ -96,21 +108,12 @@ function WorkflowsPage() { ) : ( <> {allWorkflowsData?.workflows && allWorkflowsData.workflows.length > 0 && ( -
+
-
- createMutation.mutate({ teamId })} - isLoading={createMutation.isLoading} - /> -
)} Date: Tue, 7 Feb 2023 14:51:44 -0500 Subject: [PATCH 27/64] add missing authorization checks --- .../trpc/server/routers/viewer/workflows.tsx | 41 +++++++++++++++---- .../components/createButton/CreateButton.tsx | 1 - 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index f90ee8f632325e..9276f60652fb6c 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -184,6 +184,25 @@ export const workflowsRouter = router({ const userId = !teamId ? ctx.user.id : undefined; + if (teamId) { + const team = await ctx.prisma.team.findFirst({ + where: { + id: teamId, + members: { + some: { + userId: ctx.user.id, + }, + }, + }, + }); + + if (!team) { + throw new TRPCError({ + code: "UNAUTHORIZED", + }); + } + } + try { const workflow: Workflow = await ctx.prisma.workflow.create({ data: { @@ -343,7 +362,7 @@ export const workflowsRouter = router({ } }); - //check if new event types belong to user + //check if new event types belong to user or team for (const newEventTypeId of newActiveEventTypes) { const newEventType = await ctx.prisma.eventType.findFirst({ where: { @@ -358,13 +377,19 @@ export const workflowsRouter = router({ }, }, }); - if ( - newEventType && - newEventType.userId !== user.id && - !newEventType?.team?.members.find((membership) => membership.userId === user.id) && - !newEventType?.users.find((eventTypeUser) => eventTypeUser.id === user.id) - ) { - throw new TRPCError({ code: "UNAUTHORIZED" }); + + if (newEventType) { + if (userWorkflow.teamId && userWorkflow.teamId !== newEventType.teamId) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + if ( + userWorkflow.userId && + newEventType.userId !== userWorkflow.userId && + !newEventType?.users.find((eventTypeUser) => eventTypeUser.id === userWorkflow.userId) + ) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } } } diff --git a/packages/ui/components/createButton/CreateButton.tsx b/packages/ui/components/createButton/CreateButton.tsx index ba39b9db3f5d9d..e692735e0c000d 100644 --- a/packages/ui/components/createButton/CreateButton.tsx +++ b/packages/ui/components/createButton/CreateButton.tsx @@ -10,7 +10,6 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, - DropdownMenuSeparator, DropdownMenuTrigger, } from "@calcom/ui"; import { FiPlus } from "@calcom/ui/components/icon"; From 736401a7e59502b3b000e5a8e258b08889311d84 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Tue, 7 Feb 2023 16:02:52 -0500 Subject: [PATCH 28/64] first authorization steps for workflows in event type settings --- apps/web/pages/event-types/[type]/index.tsx | 6 ++-- .../components/EventWorkflowsTab.tsx | 8 ++--- .../workflows/components/WorkflowListPage.tsx | 8 +++-- .../trpc/server/routers/viewer/workflows.tsx | 35 ++++++++++++++----- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/apps/web/pages/event-types/[type]/index.tsx b/apps/web/pages/event-types/[type]/index.tsx index fdb17d2414e22c..17cd12c46e0a8b 100644 --- a/apps/web/pages/event-types/[type]/index.tsx +++ b/apps/web/pages/event-types/[type]/index.tsx @@ -249,8 +249,10 @@ const EventTypePage = (props: EventTypeSetupProps) => { apps: , workflows: ( workflowOnEventType.workflow)} + eventType={{ ...eventType, ...{ teamId: team?.id || null } }} + workflows={eventType.workflows.map((workflowOnEventType) => { + return { ...workflowOnEventType.workflow, ...{ team } }; + })} /> ), webhooks: , diff --git a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx index 45551e10c1269e..749efebd051218 100644 --- a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx +++ b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx @@ -155,7 +155,7 @@ type Props = { function EventWorkflowsTab(props: Props) { const { workflows } = props; const { t } = useLocale(); - const { data, isLoading } = trpc.viewer.workflows.filteredList.useQuery(); + const { data, isLoading } = trpc.viewer.workflows.list.useQuery(); const router = useRouter(); const [sortedWorkflows, setSortedWorkflows] = useState>([]); @@ -164,7 +164,7 @@ function EventWorkflowsTab(props: Props) { const activeWorkflows = workflows.map((workflowOnEventType) => { return workflowOnEventType; }); - const disabledWorkflows = data.workflows.filter( + const disabledWorkflows: WorkflowType[] = data.workflows.filter( (workflow) => !workflows .map((workflow) => { @@ -176,7 +176,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 +212,7 @@ function EventWorkflowsTab(props: Props) { diff --git a/packages/features/ee/workflows/components/WorkflowListPage.tsx b/packages/features/ee/workflows/components/WorkflowListPage.tsx index 1acc2a3ae94882..c94a9d5312ddb8 100644 --- a/packages/features/ee/workflows/components/WorkflowListPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowListPage.tsx @@ -25,7 +25,11 @@ import { DeleteDialog } from "./DeleteDialog"; import EmptyScreen from "./EmptyScreen"; export type WorkflowType = Workflow & { - team: Team; + team: { + id: number; + slug: string | null; + name?: string | null; + } | null; steps: WorkflowStep[]; activeOn: { eventType: { @@ -117,7 +121,7 @@ export default function WorkflowListPage({ workflows, profiles, hasNoWorkflows } {workflow.teamId && (
  • - <>{workflow.team.name} + <>{workflow.team?.name}
  • )} diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index 9276f60652fb6c..44316a8d8d0649 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -1091,31 +1091,48 @@ action === WorkflowActions.EMAIL_ADDRESS*/ .mutation(async ({ ctx, input }) => { const { eventTypeId, workflowId } = input; - // Check that workflow & event type belong to the user + // Check that workflow & event type belong to the user or team const userEventType = await ctx.prisma.eventType.findFirst({ where: { id: eventTypeId, - users: { - some: { - id: ctx.user.id, + OR: [ + { userId: ctx.user.id }, + { + team: { + members: { + some: { + userId: ctx.user.id, + }, + }, + }, }, - }, + ], }, }); if (!userEventType) - throw new TRPCError({ code: "UNAUTHORIZED", message: "This event type does not belong to the user" }); + throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authorized to edit this event type" }); - // Check that the workflow belongs to the user + // Check that the workflow belongs to the user or team const eventTypeWorkflow = await ctx.prisma.workflow.findFirst({ where: { id: workflowId, - userId: ctx.user.id, + OR: [ + { + userId: ctx.user.id, + }, + { + teamId: userEventType.teamId, + }, + ], }, }); if (!eventTypeWorkflow) - throw new TRPCError({ code: "UNAUTHORIZED", message: "This event type does not belong to the user" }); + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Not authorized to enable/disable this workflow", + }); //check if event type is already active const isActive = await ctx.prisma.workflowsOnEventTypes.findFirst({ From 8752812f7913476a816116ac3d550de9b76d2d80 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Tue, 7 Feb 2023 17:05:01 -0500 Subject: [PATCH 29/64] show only authorized workflows in event types --- apps/web/pages/event-types/[type]/index.tsx | 1 + .../components/EventWorkflowsTab.tsx | 9 +- .../trpc/server/routers/viewer/workflows.tsx | 114 ++++++++++++++---- 3 files changed, 99 insertions(+), 25 deletions(-) diff --git a/apps/web/pages/event-types/[type]/index.tsx b/apps/web/pages/event-types/[type]/index.tsx index 17cd12c46e0a8b..37253a95638b27 100644 --- a/apps/web/pages/event-types/[type]/index.tsx +++ b/apps/web/pages/event-types/[type]/index.tsx @@ -249,6 +249,7 @@ const EventTypePage = (props: EventTypeSetupProps) => { apps: , workflows: ( { return { ...workflowOnEventType.workflow, ...{ team } }; diff --git a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx index 749efebd051218..855ce76b1f0eaf 100644 --- a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx +++ b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx @@ -145,17 +145,22 @@ const WorkflowListItem = (props: ItemProps) => { }; type Props = { + teamId?: number; eventType: { id: number; title: string; + userId: number | null; }; workflows: WorkflowType[]; }; function EventWorkflowsTab(props: Props) { - const { workflows } = props; + const { workflows, teamId, eventType } = props; const { t } = useLocale(); - const { data, isLoading } = trpc.viewer.workflows.list.useQuery(); + const { data, isLoading } = trpc.viewer.workflows.list.useQuery({ + teamId, + userId: eventType.userId || undefined, + }); const router = useRouter(); const [sortedWorkflows, setSortedWorkflows] = useState>([]); diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index 44316a8d8d0649..aa6f9660a3ddbf 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -88,12 +88,19 @@ async function isAuthorized( } export const workflowsRouter = router({ - list: authedProcedure.query(async ({ ctx }) => { - const workflows = await ctx.prisma.workflow.findMany({ - where: { - OR: [ - { userId: ctx.user.id }, - { + list: authedProcedure + .input( + z + .object({ + teamId: z.number().optional(), + userId: z.number().optional(), + }) + .optional() + ) + .query(async ({ ctx, input }) => { + if (input && input.teamId) { + const workflows = await ctx.prisma.workflow.findMany({ + where: { team: { members: { some: { @@ -102,29 +109,90 @@ export const workflowsRouter = router({ }, }, }, - ], - }, - include: { - activeOn: { - select: { - eventType: { + include: { + activeOn: { select: { - id: true, - title: true, + eventType: { + select: { + id: true, + title: true, + }, + }, + }, + }, + steps: true, + team: true, + }, + orderBy: { + id: "asc", + }, + }); + + return { workflows }; + } + + if (input && input.userId) { + const workflows = await ctx.prisma.workflow.findMany({ + where: { + userId: ctx.user.id, + }, + include: { + activeOn: { + select: { + eventType: { + select: { + id: true, + title: true, + }, + }, + }, + }, + steps: true, + team: true, + }, + orderBy: { + id: "asc", + }, + }); + return { workflows }; + } + + const workflows = await ctx.prisma.workflow.findMany({ + where: { + OR: [ + { userId: ctx.user.id }, + { + team: { + members: { + some: { + userId: ctx.user.id, + }, + }, + }, + }, + ], + }, + include: { + activeOn: { + select: { + eventType: { + select: { + id: true, + title: true, + }, }, }, }, + steps: true, + team: true, }, - steps: true, - team: true, - }, - orderBy: { - id: "asc", - }, - }); + orderBy: { + id: "asc", + }, + }); - return { workflows }; - }), + return { workflows }; + }), get: authedProcedure .input( z.object({ From 41a7ea74685e469ab32a2ee752cd25c12a3d5ff4 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Wed, 8 Feb 2023 11:34:22 -0500 Subject: [PATCH 30/64] fix create button position --- .../features/ee/workflows/pages/index.tsx | 24 +++++++++---------- .../components/createButton/CreateButton.tsx | 9 +++++-- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index 68d6e0e1cb1e7c..2ac7b946d927df 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -89,18 +89,6 @@ function WorkflowsPage() { 0 && ( - createMutation.mutate({ teamId })} - isLoading={createMutation.isLoading} - /> - ) - } subtitle={t("workflows_to_automate_notifications")}> {isLoading ? ( @@ -108,12 +96,22 @@ function WorkflowsPage() { ) : ( <> {allWorkflowsData?.workflows && allWorkflowsData.workflows.length > 0 && ( -
    +
    +
    + createMutation.mutate({ teamId })} + isLoading={createMutation.isLoading} + disableMobileButton={true} + /> +
    )} openModal(props.options[0])} data-testid="new-event-type" StartIcon={FiPlus} - variant="fab" + variant={props.disableMobileButton ? "button" : "fab"} disabled={!props.canAdd} className={props.className}> {props.buttonText ? props.buttonText : t("new")} @@ -83,7 +84,11 @@ export function CreateButton(props: CreateBtnProps) { ) : ( - From 3262ab51661faf6260b9b95c937d67a166f6d3da Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Wed, 8 Feb 2023 13:15:15 -0500 Subject: [PATCH 31/64] fix duplicate dialog --- apps/web/pages/event-types/index.tsx | 3 +++ .../eventtypes/components/DuplicateDialog.tsx | 11 +++++++++++ packages/ui/components/createButton/CreateButton.tsx | 6 +++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index b7051518f831fd..7554589a233cda 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -7,6 +7,7 @@ import { z } from "zod"; import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components"; import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog"; +import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog"; import Shell from "@calcom/features/shell/Shell"; import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -626,6 +627,7 @@ const CTA = () => { canAdd={true} options={query.data.profiles} createDialog={CreateEventTypeDialog} + duplicateDialog={DuplicateDialog} /> ); }; @@ -634,6 +636,7 @@ const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer); const EventTypesPage = () => { const { t } = useLocale(); + const router = useRouter(); const isMobile = useMediaQuery("(max-width: 768px)"); diff --git a/packages/features/eventtypes/components/DuplicateDialog.tsx b/packages/features/eventtypes/components/DuplicateDialog.tsx index a83f03027f4b4f..d2fbb01d6ccd6b 100644 --- a/packages/features/eventtypes/components/DuplicateDialog.tsx +++ b/packages/features/eventtypes/components/DuplicateDialog.tsx @@ -1,4 +1,5 @@ import { useRouter } from "next/router"; +import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -32,6 +33,16 @@ const DuplicateDialog = () => { }); const { register } = form; + useEffect(() => { + if (router.query.dialog === "duplicate") { + form.setValue("id", Number(router.query.id as string) || -1); + form.setValue("title", (router.query.title as string) || ""); + form.setValue("slug", t("event_type_duplicate_copy_text", { slug: router.query.slug as string })); + form.setValue("description", (router.query.description as string) || ""); + form.setValue("length", Number(router.query.length) || 30); + } + }, [router.query.dialog]); + const duplicateMutation = trpc.viewer.eventTypes.duplicate.useMutation({ onSuccess: async ({ eventType }) => { await router.replace("/event-types/" + eventType.id); diff --git a/packages/ui/components/createButton/CreateButton.tsx b/packages/ui/components/createButton/CreateButton.tsx index 1b5109bed1d1e7..d263e490c0cf3c 100644 --- a/packages/ui/components/createButton/CreateButton.tsx +++ b/packages/ui/components/createButton/CreateButton.tsx @@ -30,7 +30,7 @@ interface CreateBtnProps { options: Parent[]; createDialog?: () => JSX.Element; - dublicateDialog?: () => JSX.Element; + duplicateDialog?: () => JSX.Element; createFunction?: (teamId?: number) => void; subtitle?: string; className?: string; @@ -44,7 +44,7 @@ export function CreateButton(props: CreateBtnProps) { const router = useRouter(); const CreateDialog = props.createDialog ? props.createDialog() : null; - const DublicateDialog = props.dublicateDialog ? props.dublicateDialog() : null; + const DuplicateDialog = props.duplicateDialog ? props.duplicateDialog() : null; const hasTeams = !!props.options.find((option) => option.teamId); @@ -124,7 +124,7 @@ export function CreateButton(props: CreateBtnProps) { )} - {router.query.dialog === "duplicate" && DublicateDialog} + {router.query.dialog === "duplicate" && DuplicateDialog} {router.query.dialog === "new" && CreateDialog} ); From 74220ba56364c2fbca7d97ac8e82563707856ee1 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Wed, 8 Feb 2023 15:43:39 -0500 Subject: [PATCH 32/64] only show workflow of team the event type belongs to --- packages/trpc/server/routers/viewer/workflows.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index 929f9ea59fbbb0..445f5e961d7f68 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -102,6 +102,7 @@ export const workflowsRouter = router({ const workflows = await ctx.prisma.workflow.findMany({ where: { team: { + id: input.teamId, members: { some: { userId: ctx.user.id, From edec4d5f7d1ffe57e604228e869a9ed524d66585 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Wed, 8 Feb 2023 15:52:53 -0500 Subject: [PATCH 33/64] create workflows from event type settings for correct team/user --- packages/features/ee/workflows/components/EventWorkflowsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx index 855ce76b1f0eaf..1e8477371f6597 100644 --- a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx +++ b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx @@ -217,7 +217,7 @@ function EventWorkflowsTab(props: Props) { From c8c50420885be15e5eb7db263bb2fcfcc8bdef3d Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Wed, 8 Feb 2023 16:32:36 -0500 Subject: [PATCH 34/64] code clean up --- apps/web/pages/event-types/index.tsx | 1 - .../components/CreateWorkflowDialog.tsx | 42 ------------------- .../ee/workflows/components/EmptyScreen.tsx | 1 - .../workflows/components/WorkflowListPage.tsx | 1 - .../features/ee/workflows/pages/index.tsx | 6 +-- .../features/ee/workflows/pages/workflow.tsx | 6 +-- .../trpc/server/routers/viewer/workflows.tsx | 3 -- 7 files changed, 5 insertions(+), 55 deletions(-) delete mode 100644 packages/features/ee/workflows/components/CreateWorkflowDialog.tsx diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index 7554589a233cda..5f38ebd1abf1f5 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -636,7 +636,6 @@ const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer); const EventTypesPage = () => { const { t } = useLocale(); - const router = useRouter(); const isMobile = useMediaQuery("(max-width: 768px)"); diff --git a/packages/features/ee/workflows/components/CreateWorkflowDialog.tsx b/packages/features/ee/workflows/components/CreateWorkflowDialog.tsx deleted file mode 100644 index a4c48e110bc8b4..00000000000000 --- a/packages/features/ee/workflows/components/CreateWorkflowDialog.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -import { Dialog, DialogContent, DialogHeader, Form } from "@calcom/ui"; -import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui"; -import { FiZap } from "@calcom/ui/components/icon"; - -type FormValues = { - name?: string; - activeOn: Option[]; -}; - -export const CreateWorkflowDialog = () => { - const formSchema = z.object({ - name: z.string(), - activeOn: z.object({ value: z.string(), label: z.string() }).array(), - }); - - const form = useForm({ - resolver: zodResolver(formSchema), - }); - - return ( - - - <> -
    - -
    Workflow in Cal.com
    -
    - { - console.log("Create Workflow"); - }} - /> - -
    -
    - ); -}; diff --git a/packages/features/ee/workflows/components/EmptyScreen.tsx b/packages/features/ee/workflows/components/EmptyScreen.tsx index d48f8f51fdedc1..e21d0a444c5d14 100644 --- a/packages/features/ee/workflows/components/EmptyScreen.tsx +++ b/packages/features/ee/workflows/components/EmptyScreen.tsx @@ -35,7 +35,6 @@ function WorkflowExample(props: WorkflowExampleType) { export default function EmptyScreen(props: { profiles: { - membershipCount?: number | undefined; readOnly?: boolean | undefined; slug: string | null; name: string | null; diff --git a/packages/features/ee/workflows/components/WorkflowListPage.tsx b/packages/features/ee/workflows/components/WorkflowListPage.tsx index c94a9d5312ddb8..8d815bea706b10 100644 --- a/packages/features/ee/workflows/components/WorkflowListPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowListPage.tsx @@ -41,7 +41,6 @@ export type WorkflowType = Workflow & { interface Props { workflows: WorkflowType[] | undefined; profiles: { - membershipCount?: number | undefined; readOnly?: boolean | undefined; slug: string | null; name: string | null; diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index 2ac7b946d927df..7ff90a9b8911e6 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -22,8 +22,7 @@ function WorkflowsPage() { teamIds: [], }); - const allWorkflowsQuery = trpc.viewer.workflows.list.useQuery(); - const { data: allWorkflowsData, isLoading } = allWorkflowsQuery; + const { data: allWorkflowsData, isLoading } = trpc.viewer.workflows.list.useQuery(); const [filteredWorkflows, setFilteredWorkflows] = useState([]); @@ -69,7 +68,7 @@ function WorkflowsPage() { useEffect(() => { if (session.status !== "loading" && !query.isLoading) { - if (!query.data!) return; + if (!query.data) return; setCheckedFilterItems({ userId: session.data?.user.id || null, teamIds: query.data.profiles @@ -128,7 +127,6 @@ function WorkflowsPage() { const Filter = (props: { profiles: { - membershipCount?: number | undefined; readOnly?: boolean | undefined; slug: string | null; name: string | null; diff --git a/packages/features/ee/workflows/pages/workflow.tsx b/packages/features/ee/workflows/pages/workflow.tsx index 77ad135f01f31f..ccabc3d1b9eeca 100644 --- a/packages/features/ee/workflows/pages/workflow.tsx +++ b/packages/features/ee/workflows/pages/workflow.tsx @@ -20,8 +20,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; import { stringOrNumber } from "@calcom/prisma/zod-utils"; import { trpc } from "@calcom/trpc/react"; -import { Badge, MultiSelectCheckboxesOptionType as Option } from "@calcom/ui"; -import { Alert, Button, Form, showToast } from "@calcom/ui"; +import type { MultiSelectCheckboxesOptionType as Option } 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,7 +87,7 @@ function WorkflowPage() { const [selectedEventTypes, setSelectedEventTypes] = useState([]); const [isAllDataLoaded, setIsAllDataLoaded] = useState(false); - const [isMixedEventType, setIsMixedEventType] = useState(false); //for old event types before teams workflows existed + const [isMixedEventType, setIsMixedEventType] = useState(false); //for old event types before team workflows existed const form = useForm({ mode: "onBlur", diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index 445f5e961d7f68..6f53c7a8ec9d3b 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -1342,7 +1342,6 @@ action === WorkflowActions.EMAIL_ADDRESS*/ name: typeof user["name"]; }; metadata?: { - membershipCount: number; readOnly: boolean; }; workflows: typeof userWorkflows; @@ -1358,7 +1357,6 @@ action === WorkflowActions.EMAIL_ADDRESS*/ }, workflows: userWorkflows, metadata: { - membershipCount: 1, readOnly: false, }, }); @@ -1372,7 +1370,6 @@ action === WorkflowActions.EMAIL_ADDRESS*/ slug: "team/" + membership.team.slug, }, metadata: { - membershipCount: membership.team.members.length, readOnly: membership.role === MembershipRole.MEMBER, }, workflows: membership.team.workflows, From 6218466535a12cdc9d2ba633e579b045a1e58536 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Fri, 10 Feb 2023 08:43:13 -0500 Subject: [PATCH 35/64] remove classname from createButton props --- .../ee/workflows/components/EmptyScreen.tsx | 19 ++++++++++--------- .../components/createButton/CreateButton.tsx | 5 +---- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/features/ee/workflows/components/EmptyScreen.tsx b/packages/features/ee/workflows/components/EmptyScreen.tsx index e21d0a444c5d14..131a21cb8ce179 100644 --- a/packages/features/ee/workflows/components/EmptyScreen.tsx +++ b/packages/features/ee/workflows/components/EmptyScreen.tsx @@ -87,15 +87,16 @@ export default function EmptyScreen(props: {

    {t("no_workflows_description")}

    - createMutation.mutate({ teamId })} - buttonText={t("create_workflow")} - isLoading={createMutation.isLoading} - /> +
    + createMutation.mutate({ teamId })} + buttonText={t("create_workflow")} + isLoading={createMutation.isLoading} + /> +
    diff --git a/packages/ui/components/createButton/CreateButton.tsx b/packages/ui/components/createButton/CreateButton.tsx index d263e490c0cf3c..471f4ca8dbccf9 100644 --- a/packages/ui/components/createButton/CreateButton.tsx +++ b/packages/ui/components/createButton/CreateButton.tsx @@ -33,7 +33,6 @@ interface CreateBtnProps { duplicateDialog?: () => JSX.Element; createFunction?: (teamId?: number) => void; subtitle?: string; - className?: string; buttonText?: string; isLoading?: boolean; disableMobileButton?: boolean; @@ -77,8 +76,7 @@ export function CreateButton(props: CreateBtnProps) { data-testid="new-event-type" StartIcon={FiPlus} variant={props.disableMobileButton ? "button" : "fab"} - disabled={!props.canAdd} - className={props.className}> + disabled={!props.canAdd}> {props.buttonText ? props.buttonText : t("new")} ) : ( @@ -87,7 +85,6 @@ export function CreateButton(props: CreateBtnProps) { From c50da51c558872e0cde0463905c4b7beebfca815 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Fri, 10 Feb 2023 08:47:51 -0500 Subject: [PATCH 36/64] code clean up --- apps/web/pages/event-types/[type]/index.tsx | 5 ++--- .../ee/workflows/components/EventWorkflowsTab.tsx | 10 ++++++---- packages/lib/getEventTypeById.ts | 7 +++++++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/apps/web/pages/event-types/[type]/index.tsx b/apps/web/pages/event-types/[type]/index.tsx index 37253a95638b27..a0164bd748453c 100644 --- a/apps/web/pages/event-types/[type]/index.tsx +++ b/apps/web/pages/event-types/[type]/index.tsx @@ -249,10 +249,9 @@ const EventTypePage = (props: EventTypeSetupProps) => { apps: , workflows: ( { - return { ...workflowOnEventType.workflow, ...{ team } }; + return workflowOnEventType.workflow; })} /> ), diff --git a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx index 1e8477371f6597..4e893d068d4be9 100644 --- a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx +++ b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx @@ -145,20 +145,22 @@ const WorkflowListItem = (props: ItemProps) => { }; type Props = { - teamId?: number; eventType: { id: number; title: string; userId: number | null; + team: { + id?: number; + } | null; }; workflows: WorkflowType[]; }; function EventWorkflowsTab(props: Props) { - const { workflows, teamId, eventType } = props; + const { workflows, eventType } = props; const { t } = useLocale(); const { data, isLoading } = trpc.viewer.workflows.list.useQuery({ - teamId, + teamId: eventType.team?.id, userId: eventType.userId || undefined, }); const router = useRouter(); @@ -217,7 +219,7 @@ function EventWorkflowsTab(props: Props) { diff --git a/packages/lib/getEventTypeById.ts b/packages/lib/getEventTypeById.ts index 9eb70e47ca3b33..5438bfc51f7bc2 100644 --- a/packages/lib/getEventTypeById.ts +++ b/packages/lib/getEventTypeById.ts @@ -151,6 +151,13 @@ export default async function getEventTypeById({ include: { workflow: { include: { + team: { + select: { + id: true, + slug: true, + name: true, + }, + }, activeOn: { select: { eventType: { From e6a0f45a7701fe7c324ea6283bbb195a65a28a7b Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Fri, 10 Feb 2023 08:58:29 -0500 Subject: [PATCH 37/64] code clean up --- apps/web/pages/event-types/[type]/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/web/pages/event-types/[type]/index.tsx b/apps/web/pages/event-types/[type]/index.tsx index a0164bd748453c..fdb17d2414e22c 100644 --- a/apps/web/pages/event-types/[type]/index.tsx +++ b/apps/web/pages/event-types/[type]/index.tsx @@ -250,9 +250,7 @@ const EventTypePage = (props: EventTypeSetupProps) => { workflows: ( { - return workflowOnEventType.workflow; - })} + workflows={eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow)} /> ), webhooks: , From 02a3dca36c768a652b57162af3b7344ecf69a82d Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Fri, 10 Feb 2023 09:31:56 -0500 Subject: [PATCH 38/64] text alignment + code clean up --- .../ee/workflows/components/EmptyScreen.tsx | 3 +-- .../components/EventWorkflowsTab.tsx | 2 +- .../features/ee/workflows/pages/index.tsx | 23 +++++++++---------- .../components/createButton/CreateButton.tsx | 13 +++-------- 4 files changed, 16 insertions(+), 25 deletions(-) diff --git a/packages/features/ee/workflows/components/EmptyScreen.tsx b/packages/features/ee/workflows/components/EmptyScreen.tsx index 131a21cb8ce179..d899278eeb7f72 100644 --- a/packages/features/ee/workflows/components/EmptyScreen.tsx +++ b/packages/features/ee/workflows/components/EmptyScreen.tsx @@ -87,10 +87,9 @@ export default function EmptyScreen(props: {

    {t("no_workflows_description")}

    -
    +
    createMutation.mutate({ teamId })} buttonText={t("create_workflow")} diff --git a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx index 4e893d068d4be9..8bba40a9a21d3f 100644 --- a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx +++ b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx @@ -171,7 +171,7 @@ function EventWorkflowsTab(props: Props) { const activeWorkflows = workflows.map((workflowOnEventType) => { return workflowOnEventType; }); - const disabledWorkflows: WorkflowType[] = data.workflows.filter( + const disabledWorkflows = data.workflows.filter( (workflow) => !workflows .map((workflow) => { diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index 7ff90a9b8911e6..32847c204c2b12 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -26,16 +26,6 @@ function WorkflowsPage() { const [filteredWorkflows, setFilteredWorkflows] = useState([]); - useEffect(() => { - if (allWorkflowsData?.workflows) { - const filtered = allWorkflowsData?.workflows.filter((workflow) => { - if (!!workflow.userId && workflow.userId === checkedFilterItems.userId) return workflow; - if (checkedFilterItems.teamIds.includes(workflow.teamId || 0)) return workflow; - }); - setFilteredWorkflows(filtered); - } - }, [allWorkflowsData]); - const createMutation = trpc.viewer.workflows.create.useMutation({ onSuccess: async ({ workflow }) => { await router.replace("/workflows/" + workflow.id); @@ -55,6 +45,16 @@ function WorkflowsPage() { const query = trpc.viewer.workflows.getByViewer.useQuery(); + useEffect(() => { + if (allWorkflowsData?.workflows) { + const filtered = allWorkflowsData?.workflows.filter((workflow) => { + if (!!workflow.userId && workflow.userId === checkedFilterItems.userId) return workflow; + if (checkedFilterItems.teamIds.includes(workflow.teamId || 0)) return workflow; + }); + setFilteredWorkflows(filtered); + } + }, [allWorkflowsData]); + useEffect(() => { const allWorkflows = allWorkflowsData?.workflows; if (allWorkflows && allWorkflows.length > 0) { @@ -80,7 +80,7 @@ function WorkflowsPage() { .filter((teamId) => !!teamId) as number[], }); } - }, [session.status, query.isLoading]); + }, [session.status, query.isLoading, allWorkflowsData]); if (!query.data) return null; @@ -104,7 +104,6 @@ function WorkflowsPage() {
    createMutation.mutate({ teamId })} isLoading={createMutation.isLoading} diff --git a/packages/ui/components/createButton/CreateButton.tsx b/packages/ui/components/createButton/CreateButton.tsx index 471f4ca8dbccf9..74a16a1a790043 100644 --- a/packages/ui/components/createButton/CreateButton.tsx +++ b/packages/ui/components/createButton/CreateButton.tsx @@ -22,13 +22,7 @@ export interface Parent { } interface CreateBtnProps { - // set true for use on the team settings page - canAdd: boolean; - // set true when in use on the team settings page - isIndividualTeam?: boolean; - // EventTypeParent can be a profile (as first option) or a team for the rest. options: Parent[]; - createDialog?: () => JSX.Element; duplicateDialog?: () => JSX.Element; createFunction?: (teamId?: number) => void; @@ -70,13 +64,12 @@ export function CreateButton(props: CreateBtnProps) { return ( <> - {!hasTeams || props.isIndividualTeam ? ( + {!hasTeams ? ( ) : ( @@ -91,7 +84,7 @@ export function CreateButton(props: CreateBtnProps) { -
    {props.subtitle}
    +
    {props.subtitle}
    {props.options.map((option) => ( From 1dccd1d2154f0ddc8fbadd5df0195b05c3f256ff Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Fri, 10 Feb 2023 09:32:22 -0500 Subject: [PATCH 39/64] add migration --- .../20230210132534_add_workflows_to_teams/migration.sql | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 packages/prisma/migrations/20230210132534_add_workflows_to_teams/migration.sql diff --git a/packages/prisma/migrations/20230210132534_add_workflows_to_teams/migration.sql b/packages/prisma/migrations/20230210132534_add_workflows_to_teams/migration.sql new file mode 100644 index 00000000000000..5a0475a2d89c01 --- /dev/null +++ b/packages/prisma/migrations/20230210132534_add_workflows_to_teams/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "Workflow" ADD COLUMN "teamId" INTEGER, +ALTER COLUMN "userId" DROP NOT NULL; + +-- AddForeignKey +ALTER TABLE "Workflow" ADD CONSTRAINT "Workflow_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE; From b2928442c915da65b9d6a4a30610fa251de60b9a Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Fri, 10 Feb 2023 10:14:28 -0500 Subject: [PATCH 40/64] code clean up --- apps/web/pages/event-types/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index 0668a37f65bd19..d36df6402bbcdd 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -622,7 +622,6 @@ const CTA = () => { return ( Date: Fri, 10 Feb 2023 10:40:34 -0500 Subject: [PATCH 41/64] fix duplicate dialog --- packages/features/eventtypes/components/DuplicateDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/features/eventtypes/components/DuplicateDialog.tsx b/packages/features/eventtypes/components/DuplicateDialog.tsx index 75ec16c9bcdbb5..e856252b736d01 100644 --- a/packages/features/eventtypes/components/DuplicateDialog.tsx +++ b/packages/features/eventtypes/components/DuplicateDialog.tsx @@ -80,7 +80,7 @@ const DuplicateDialog = () => { return ( Date: Fri, 10 Feb 2023 10:53:56 -0500 Subject: [PATCH 42/64] remove not needed useEffect --- packages/features/ee/workflows/pages/index.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index 32847c204c2b12..40f9724ef4fb1a 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -45,16 +45,6 @@ function WorkflowsPage() { const query = trpc.viewer.workflows.getByViewer.useQuery(); - useEffect(() => { - if (allWorkflowsData?.workflows) { - const filtered = allWorkflowsData?.workflows.filter((workflow) => { - if (!!workflow.userId && workflow.userId === checkedFilterItems.userId) return workflow; - if (checkedFilterItems.teamIds.includes(workflow.teamId || 0)) return workflow; - }); - setFilteredWorkflows(filtered); - } - }, [allWorkflowsData]); - useEffect(() => { const allWorkflows = allWorkflowsData?.workflows; if (allWorkflows && allWorkflows.length > 0) { From cad331c10a7cd85579c12d2117a2fc10ad09dc4f Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Fri, 10 Feb 2023 11:19:54 -0500 Subject: [PATCH 43/64] only show filter if user has at least one team --- packages/features/ee/workflows/pages/index.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index 40f9724ef4fb1a..1e372b4192e1cf 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -86,11 +86,13 @@ function WorkflowsPage() { <> {allWorkflowsData?.workflows && allWorkflowsData.workflows.length > 0 && (
    - + {query.data.profiles.length > 1 && ( + + )}
    Date: Fri, 10 Feb 2023 11:35:49 -0500 Subject: [PATCH 44/64] fix create button without teams --- .../features/ee/workflows/pages/index.tsx | 49 +++++++++++++------ .../components/createButton/CreateButton.tsx | 9 +++- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index 1e372b4192e1cf..5e63bdd54d06a5 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -78,32 +78,51 @@ function WorkflowsPage() { + subtitle={t("workflows_to_automate_notifications")} + CTA={ + query.data.profiles.length === 1 && + session.data?.hasValidLicense && + allWorkflowsData?.workflows && + allWorkflowsData?.workflows.length > 0 ? ( + { + console.log("test test"); + createMutation.mutate({ teamId }); + }} + isLoading={createMutation.isLoading} + disableMobileButton={true} + /> + ) : ( + <> + ) + }> {isLoading ? ( ) : ( <> - {allWorkflowsData?.workflows && allWorkflowsData.workflows.length > 0 && ( -
    - {query.data.profiles.length > 1 && ( + {query.data.profiles.length > 1 && + allWorkflowsData?.workflows && + allWorkflowsData.workflows.length > 0 && ( +
    - )} -
    - createMutation.mutate({ teamId })} - isLoading={createMutation.isLoading} - disableMobileButton={true} - /> +
    + createMutation.mutate({ teamId })} + isLoading={createMutation.isLoading} + disableMobileButton={true} + /> +
    -
    - )} + )} {!hasTeams ? ( From e9dedd2764368b1899f507eea67321f11489e743 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Fri, 10 Feb 2023 14:36:32 -0500 Subject: [PATCH 45/64] add teamId to verified numbers --- .../components/WorkflowDetailsPage.tsx | 3 +- .../components/WorkflowStepContainer.tsx | 9 +- .../lib/reminders/reminderScheduler.ts | 2 + .../lib/reminders/smsReminderManager.ts | 8 +- .../lib/reminders/verifyPhoneNumber.ts | 10 +- .../features/ee/workflows/pages/workflow.tsx | 8 +- .../migration.sql | 6 + packages/prisma/schema.prisma | 21 ++-- .../trpc/server/routers/viewer/workflows.tsx | 115 ++++++++++-------- 9 files changed, 112 insertions(+), 70 deletions(-) create mode 100644 packages/prisma/migrations/20230210182245_add_verified_numbers_to_team/migration.sql diff --git a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx index 961dc00de8e660..c7521980236252 100644 --- a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx @@ -152,7 +152,7 @@ export default function WorkflowDetailsPage(props: Props) {
    {form.getValues("trigger") && (
    - +
    )} {form.getValues("steps") && ( @@ -165,6 +165,7 @@ export default function WorkflowDetailsPage(props: Props) { step={step} reload={reload} setReload={setReload} + teamId={teamId} /> ); })} diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index 3fd3e6f7f85109..688a31c83ffa34 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -51,17 +51,17 @@ type WorkflowStepProps = { form: UseFormReturn; reload?: boolean; setReload?: Dispatch>; + 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 { step, form, reload, setReload, teamId } = props; + const { data: _verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery({ teamId }); const verifiedNumbers = _verifiedNumbers?.map((number) => number.phoneNumber); const [isAdditionalInputsDialogOpen, setIsAdditionalInputsDialogOpen] = useState(false); - const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); const [verificationCode, setVerificationCode] = useState(""); @@ -455,6 +455,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { verifyPhoneNumberMutation.mutate({ phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "", code: verificationCode, + teamId, }); }}> Verify diff --git a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts index 769ac11f412900..6ef3f02b612f70 100644 --- a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts +++ b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts @@ -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 ( diff --git a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts index b79dd391c61159..cd0b8d541ff54e 100644 --- a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts @@ -54,7 +54,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; @@ -70,7 +71,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; diff --git a/packages/features/ee/workflows/lib/reminders/verifyPhoneNumber.ts b/packages/features/ee/workflows/lib/reminders/verifyPhoneNumber.ts index 4f911cd59a3ff4..6041747e5dad8e 100644 --- a/packages/features/ee/workflows/lib/reminders/verifyPhoneNumber.ts +++ b/packages/features/ee/workflows/lib/reminders/verifyPhoneNumber.ts @@ -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, }, }); diff --git a/packages/features/ee/workflows/pages/workflow.tsx b/packages/features/ee/workflows/pages/workflow.tsx index ccabc3d1b9eeca..6ed5ada834093a 100644 --- a/packages/features/ee/workflows/pages/workflow.tsx +++ b/packages/features/ee/workflows/pages/workflow.tsx @@ -102,6 +102,7 @@ function WorkflowPage() { isError, error, dataUpdatedAt, + isLoading, } = trpc.viewer.workflows.get.useQuery( { id: +workflowId }, { @@ -109,7 +110,12 @@ function WorkflowPage() { } ); - const { data: verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery(); + const { data: verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery( + { teamId: workflow?.team?.id }, + { + enabled: !isLoading, + } + ); useEffect(() => { if (workflow && (workflow.steps.length === 0 || workflow.steps[0].stepNumber === 1)) { diff --git a/packages/prisma/migrations/20230210182245_add_verified_numbers_to_team/migration.sql b/packages/prisma/migrations/20230210182245_add_verified_numbers_to_team/migration.sql new file mode 100644 index 00000000000000..f381d6548cfaf5 --- /dev/null +++ b/packages/prisma/migrations/20230210182245_add_verified_numbers_to_team/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "VerifiedNumber" ADD COLUMN "teamId" INTEGER, +ALTER COLUMN "userId" DROP NOT NULL; + +-- AddForeignKey +ALTER TABLE "VerifiedNumber" ADD CONSTRAINT "VerifiedNumber_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 74850d8e8a69b6..930aa1568a6916 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -203,24 +203,25 @@ model User { } model Team { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) /// @zod.min(1) name String /// @zod.min(1) - slug String? @unique + slug String? @unique logo String? bio String? - hideBranding Boolean @default(false) - hideBookATeamMember Boolean @default(false) + hideBranding Boolean @default(false) + hideBookATeamMember Boolean @default(false) members Membership[] eventTypes EventType[] workflows Workflow[] - createdAt DateTime @default(now()) + createdAt DateTime @default(now()) /// @zod.custom(imports.teamMetadataSchema) metadata Json? theme String? - brandColor String @default("#292929") - darkBrandColor String @default("#fafafa") + brandColor String @default("#292929") + darkBrandColor String @default("#fafafa") + verifiedNumbers VerifiedNumber[] } enum MembershipRole { @@ -658,7 +659,9 @@ enum WorkflowMethods { model VerifiedNumber { id Int @id @default(autoincrement()) - userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int? + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + teamId Int? + team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) phoneNumber String } diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index 6f53c7a8ec9d3b..aff90e72e9dc50 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -632,7 +632,8 @@ export const workflowsRouter = router({ step.id, step.template, step.sender || SENDER_ID, - user.id + user.id, + userWorkflow.teamId ); } }); @@ -682,6 +683,7 @@ export const workflowsRouter = router({ //step was edited } else if (JSON.stringify(oldStep) !== JSON.stringify(newStep)) { if ( + !userWorkflow.teamId && !userWorkflow.user?.teams.length && !isSMSAction(oldStep.action) && isSMSAction(newStep.action) @@ -829,7 +831,8 @@ export const workflowsRouter = router({ newStep.id || 0, newStep.template, newStep.sender || SENDER_ID, - user.id + user.id, + userWorkflow.teamId ); } }); @@ -956,7 +959,8 @@ export const workflowsRouter = router({ createdStep.id, step.template, step.sender || SENDER_ID, - user.id + user.id, + userWorkflow.teamId ); } }); @@ -1058,63 +1062,63 @@ export const workflowsRouter = router({ if (isSMSAction(step.action) /*|| step.action === WorkflowActions.EMAIL_ADDRESS*/ /*) { const hasTeamPlan = (await ctx.prisma.membership.count({ where: { userId: user.id } })) > 0; if (!hasTeamPlan) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Team plan needed" }); +throw new TRPCError({ code: "UNAUTHORIZED", message: "Team plan needed" }); } } const booking = await ctx.prisma.booking.findFirst({ orderBy: { - createdAt: "desc", +createdAt: "desc", }, where: { - userId: ctx.user.id, +userId: ctx.user.id, }, include: { - attendees: true, - user: true, +attendees: true, +user: true, }, }); let evt: BookingInfo; if (booking) { evt = { - uid: booking?.uid, - attendees: - booking?.attendees.map((attendee) => { - return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone }; - }) || [], - organizer: { - language: { - locale: booking?.user?.locale || "", - }, - name: booking?.user?.name || "", - email: booking?.user?.email || "", - timeZone: booking?.user?.timeZone || "", - }, - startTime: booking?.startTime.toISOString() || "", - endTime: booking?.endTime.toISOString() || "", - title: booking?.title || "", - location: booking?.location || null, - additionalNotes: booking?.description || null, - customInputs: booking?.customInputs, +uid: booking?.uid, +attendees: +booking?.attendees.map((attendee) => { + return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone }; +}) || [], +organizer: { +language: { + locale: booking?.user?.locale || "", +}, +name: booking?.user?.name || "", +email: booking?.user?.email || "", +timeZone: booking?.user?.timeZone || "", +}, +startTime: booking?.startTime.toISOString() || "", +endTime: booking?.endTime.toISOString() || "", +title: booking?.title || "", +location: booking?.location || null, +additionalNotes: booking?.description || null, +customInputs: booking?.customInputs, }; } else { //if no booking exists create an example booking evt = { - attendees: [{ name: "John Doe", email: "john.doe@example.com", timeZone: "Europe/London" }], - organizer: { - language: { - locale: ctx.user.locale, - }, - name: ctx.user.name || "", - email: ctx.user.email, - timeZone: ctx.user.timeZone, - }, - startTime: dayjs().add(10, "hour").toISOString(), - endTime: dayjs().add(11, "hour").toISOString(), - title: "Example Booking", - location: "Office", - additionalNotes: "These are additional notes", +attendees: [{ name: "John Doe", email: "john.doe@example.com", timeZone: "Europe/London" }], +organizer: { +language: { + locale: ctx.user.locale, +}, +name: ctx.user.name || "", +email: ctx.user.email, +timeZone: ctx.user.timeZone, +}, +startTime: dayjs().add(10, "hour").toISOString(), +endTime: dayjs().add(11, "hour").toISOString(), +title: "Example Booking", +location: "Office", +additionalNotes: "These are additional notes", }; } @@ -1257,24 +1261,31 @@ action === WorkflowActions.EMAIL_ADDRESS*/ z.object({ phoneNumber: z.string(), code: z.string(), + teamId: z.number().optional(), }) ) .mutation(async ({ ctx, input }) => { - const { phoneNumber, code } = input; + const { phoneNumber, code, teamId } = input; const { user } = ctx; - const verifyStatus = await verifyPhoneNumber(phoneNumber, code, user.id); + const verifyStatus = await verifyPhoneNumber(phoneNumber, code, user.id, teamId); return verifyStatus; }), - getVerifiedNumbers: authedProcedure.query(async ({ ctx }) => { - const { user } = ctx; - const verifiedNumbers = await ctx.prisma.verifiedNumber.findMany({ - where: { - userId: user.id, - }, - }); + getVerifiedNumbers: authedProcedure + .input( + z.object({ + teamId: z.number().optional(), + }) + ) + .query(async ({ ctx, input }) => { + const { user } = ctx; + const verifiedNumbers = await ctx.prisma.verifiedNumber.findMany({ + where: { + OR: [{ userId: user.id }, { teamId: input.teamId }], + }, + }); - return verifiedNumbers; - }), + return verifiedNumbers; + }), getWorkflowActionOptions: authedProcedure.query(async ({ ctx }) => { const { hasTeamPlan } = await viewerTeamsRouter.createCaller(ctx).hasTeamPlan(); const t = await getTranslation(ctx.user.locale, "common"); From fd2eae20c094f11091e7863b8533a468ba958fe6 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Mon, 13 Feb 2023 11:13:27 -0500 Subject: [PATCH 46/64] fix verified phone numbers for teams --- .../components/WorkflowStepContainer.tsx | 15 +++++++++++++-- packages/features/ee/workflows/pages/workflow.tsx | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index 688a31c83ffa34..a7a82689cb2819 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -5,7 +5,7 @@ import { WorkflowTemplates, WorkflowTriggerEvents, } from "@prisma/client"; -import { Dispatch, SetStateAction, useRef, useState } from "react"; +import { Dispatch, SetStateAction, useRef, useState, useEffect } from "react"; import { Controller, UseFormReturn } from "react-hook-form"; import "react-phone-number-input/style.css"; @@ -59,7 +59,10 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { const utils = trpc.useContext(); const { step, form, reload, setReload, teamId } = props; - const { data: _verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery({ teamId }); + const { data: _verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery( + { teamId }, + { enabled: !!teamId } + ); const verifiedNumbers = _verifiedNumbers?.map((number) => number.phoneNumber); const [isAdditionalInputsDialogOpen, setIsAdditionalInputsDialogOpen] = useState(false); @@ -75,6 +78,14 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { : false ); + useEffect(() => { + setNumberVerified( + !!step && verifiedNumbers + ? !!verifiedNumbers.find((number) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`)) + : false + ); + }, [verifiedNumbers]); + const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState( step?.action === WorkflowActions.EMAIL_ADDRESS ? true : false ); diff --git a/packages/features/ee/workflows/pages/workflow.tsx b/packages/features/ee/workflows/pages/workflow.tsx index ecbbdd4bca06d2..14af0fb04b7b9a 100644 --- a/packages/features/ee/workflows/pages/workflow.tsx +++ b/packages/features/ee/workflows/pages/workflow.tsx @@ -112,7 +112,7 @@ function WorkflowPage() { const { data: verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery( { teamId: workflow?.team?.id }, { - enabled: !isLoading, + enabled: !!workflow?.id, } ); From 87165b313349a34d99cd3f7bb20175c003c0304f Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Mon, 13 Feb 2023 13:37:57 -0500 Subject: [PATCH 47/64] code clean up --- .../workflows/components/WorkflowStepContainer.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index a7a82689cb2819..aade26a9c837da 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -63,7 +63,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { { teamId }, { enabled: !!teamId } ); - const verifiedNumbers = _verifiedNumbers?.map((number) => number.phoneNumber); + const verifiedNumbers = _verifiedNumbers?.map((number) => number.phoneNumber) || []; const [isAdditionalInputsDialogOpen, setIsAdditionalInputsDialogOpen] = useState(false); const [verificationCode, setVerificationCode] = useState(""); @@ -80,11 +80,10 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { useEffect(() => { setNumberVerified( - !!step && verifiedNumbers - ? !!verifiedNumbers.find((number) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`)) - : false + !!step && + !!verifiedNumbers.find((number) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`)) ); - }, [verifiedNumbers]); + }, [verifiedNumbers.length]); const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState( step?.action === WorkflowActions.EMAIL_ADDRESS ? true : false @@ -127,9 +126,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { const refReminderBody = useRef(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) => { From 29557d993884e714925ef75b3d1c92b1298b9174 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Tue, 14 Feb 2023 09:57:27 -0500 Subject: [PATCH 48/64] fixing list update after deleting last workflow --- packages/features/ee/workflows/pages/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index 5e63bdd54d06a5..8f28912ad96c21 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -53,8 +53,10 @@ function WorkflowsPage() { if (checkedFilterItems.teamIds.includes(workflow.teamId || 0)) return workflow; }); setFilteredWorkflows(filtered); + } else { + setFilteredWorkflows([]); } - }, [checkedFilterItems]); + }, [checkedFilterItems, allWorkflowsData]); useEffect(() => { if (session.status !== "loading" && !query.isLoading) { From 0325bd7365871dc12f0495dee5086d1cdb971c46 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Sun, 19 Feb 2023 14:17:14 +0100 Subject: [PATCH 49/64] Update packages/features/ee/workflows/components/EmptyScreen.tsx Co-authored-by: Hariom Balhara --- packages/features/ee/workflows/components/EmptyScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/features/ee/workflows/components/EmptyScreen.tsx b/packages/features/ee/workflows/components/EmptyScreen.tsx index d899278eeb7f72..9194fd7ab9fb34 100644 --- a/packages/features/ee/workflows/components/EmptyScreen.tsx +++ b/packages/features/ee/workflows/components/EmptyScreen.tsx @@ -56,7 +56,7 @@ export default function EmptyScreen(props: { } if (err.data?.code === "UNAUTHORIZED") { - const message = `${err.data.code}: You are not able to create this workflow`; + const message = `${err.data.code}: You are not authorized to create this workflow`; showToast(message, "error"); } }, From 59c4ac131e911818f984202c029d491cf4d65a1e Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Mon, 20 Feb 2023 14:05:19 -0500 Subject: [PATCH 50/64] check if membership is accepted + type predicate feedback --- packages/features/ee/workflows/pages/index.tsx | 2 +- packages/trpc/server/routers/viewer/workflows.tsx | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index 8f28912ad96c21..c59f930b6d2e8c 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -69,7 +69,7 @@ function WorkflowsPage() { return profile.teamId; } }) - .filter((teamId) => !!teamId) as number[], + .filter((teamId): teamId is number => !!teamId), }); } }, [session.status, query.isLoading, allWorkflowsData]); diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index 710ac5b56f058e..06daee3c3c1ce9 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -74,6 +74,7 @@ async function isAuthorized( members: { some: { userId: currentUserId, + accepted: true, }, }, }, @@ -106,6 +107,7 @@ export const workflowsRouter = router({ members: { some: { userId: ctx.user.id, + accepted: true, }, }, }, @@ -167,6 +169,7 @@ export const workflowsRouter = router({ members: { some: { userId: ctx.user.id, + accepted: true, }, }, }, @@ -260,6 +263,7 @@ export const workflowsRouter = router({ members: { some: { userId: ctx.user.id, + accepted: true, }, }, }, @@ -1194,6 +1198,7 @@ action === WorkflowActions.EMAIL_ADDRESS*/ members: { some: { userId: ctx.user.id, + accepted: true, }, }, }, @@ -1353,8 +1358,8 @@ action === WorkflowActions.EMAIL_ADDRESS*/ type WorkflowGroup = { teamId?: number | null; profile: { - slug: typeof user["username"]; - name: typeof user["name"]; + slug: (typeof user)["username"]; + name: (typeof user)["name"]; }; metadata?: { readOnly: boolean; From b22d0ba0347d51223e188a20e4657fb9034702e4 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Mon, 20 Feb 2023 16:15:42 -0500 Subject: [PATCH 51/64] always save userId --- packages/features/ee/workflows/pages/index.tsx | 4 +++- packages/trpc/server/routers/viewer/workflows.tsx | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index c59f930b6d2e8c..60bc254e13c93d 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -49,8 +49,10 @@ function WorkflowsPage() { const allWorkflows = allWorkflowsData?.workflows; if (allWorkflows && allWorkflows.length > 0) { const filtered = allWorkflows.filter((workflow) => { - if (!!workflow.userId && workflow.userId === checkedFilterItems.userId) return 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 { diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index 06daee3c3c1ce9..eec587e650954a 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -254,7 +254,7 @@ export const workflowsRouter = router({ .mutation(async ({ ctx, input }) => { const { teamId } = input; - const userId = !teamId ? ctx.user.id : undefined; + const userId = ctx.user.id; if (teamId) { const team = await ctx.prisma.team.findFirst({ @@ -457,6 +457,7 @@ export const workflowsRouter = router({ } if ( + !userWorkflow.teamId && userWorkflow.userId && newEventType.userId !== userWorkflow.userId && !newEventType?.users.find((eventTypeUser) => eventTypeUser.id === userWorkflow.userId) From 97f0ed7bbca2c670226215a3e53f562d426a9485 Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Mon, 20 Feb 2023 16:19:55 -0500 Subject: [PATCH 52/64] Update packages/ui/components/createButton/CreateButton.tsx Co-authored-by: Hariom Balhara --- packages/ui/components/createButton/CreateButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/components/createButton/CreateButton.tsx b/packages/ui/components/createButton/CreateButton.tsx index 4f692a6b0c9118..97d616f0b8546c 100644 --- a/packages/ui/components/createButton/CreateButton.tsx +++ b/packages/ui/components/createButton/CreateButton.tsx @@ -97,7 +97,7 @@ export function CreateButton(props: CreateBtnProps) { ( + StartIcon={(props) => ( Date: Tue, 21 Feb 2023 09:10:49 -0500 Subject: [PATCH 53/64] improve createButton props --- apps/web/pages/event-types/index.tsx | 6 ++- .../ee/workflows/components/EmptyScreen.tsx | 11 +++-- .../workflows/components/WorkflowListPage.tsx | 16 ++++---- .../features/ee/workflows/pages/index.tsx | 21 ++++++---- .../trpc/server/routers/viewer/eventTypes.ts | 2 + .../trpc/server/routers/viewer/workflows.tsx | 41 ++++++++++--------- .../components/createButton/CreateButton.tsx | 19 ++++----- 7 files changed, 62 insertions(+), 54 deletions(-) diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index fbac5c2a09017f..003fb2faabd72c 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -636,10 +636,14 @@ const CTA = () => { if (!query.data) return null; + const profileOptions = query.data.profiles.map((profile) => { + return { teamId: profile.teamId, label: profile.name || profile.slug, image: profile.image }; + }); + return ( diff --git a/packages/features/ee/workflows/components/EmptyScreen.tsx b/packages/features/ee/workflows/components/EmptyScreen.tsx index 9194fd7ab9fb34..b0de24999fec2a 100644 --- a/packages/features/ee/workflows/components/EmptyScreen.tsx +++ b/packages/features/ee/workflows/components/EmptyScreen.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; import { trpc } from "@calcom/trpc/react"; -import { SVGComponent } from "@calcom/types/SVGComponent"; +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"; @@ -34,10 +34,9 @@ function WorkflowExample(props: WorkflowExampleType) { } export default function EmptyScreen(props: { - profiles: { - readOnly?: boolean | undefined; - slug: string | null; - name: string | null; + profileOptions: { + label: string | null; + image?: string | null; teamId: number | null | undefined; }[]; isFilteredView: boolean; @@ -90,7 +89,7 @@ export default function EmptyScreen(props: {
    createMutation.mutate({ teamId })} buttonText={t("create_workflow")} isLoading={createMutation.isLoading} diff --git a/packages/features/ee/workflows/components/WorkflowListPage.tsx b/packages/features/ee/workflows/components/WorkflowListPage.tsx index 8d815bea706b10..2d65220f829ed5 100644 --- a/packages/features/ee/workflows/components/WorkflowListPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowListPage.tsx @@ -1,11 +1,10 @@ -import { Workflow, WorkflowStep } from "@prisma/client"; +import type { Workflow, WorkflowStep } from "@prisma/client"; 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 { Team } from "@calcom/prisma/client"; import { trpc } from "@calcom/trpc/react"; import { Button, @@ -18,7 +17,7 @@ import { 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"; @@ -40,15 +39,14 @@ export type WorkflowType = Workflow & { }; interface Props { workflows: WorkflowType[] | undefined; - profiles: { - readOnly?: boolean | undefined; - slug: string | null; - name: string | null; + profileOptions: { + image?: string | null; + label: string | null; teamId: number | null | undefined; }[]; hasNoWorkflows?: boolean; } -export default function WorkflowListPage({ workflows, profiles, hasNoWorkflows }: Props) { +export default function WorkflowListPage({ workflows, profileOptions, hasNoWorkflows }: Props) { const { t } = useLocale(); const utils = trpc.useContext(); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); @@ -196,7 +194,7 @@ export default function WorkflowListPage({ workflows, profiles, hasNoWorkflows } />
    ) : ( - + )} ); diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index 60bc254e13c93d..05b969137dcc31 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -1,6 +1,7 @@ import { useSession } from "next-auth/react"; import { useRouter } from "next/router"; -import { useState, Dispatch, SetStateAction, useEffect } from "react"; +import type { Dispatch, SetStateAction } from "react"; +import { useState, useEffect } from "react"; import Shell from "@calcom/features/shell/Shell"; import { classNames } from "@calcom/lib"; @@ -11,7 +12,8 @@ import { AnimatedPopover, Avatar, CreateButton, showToast } from "@calcom/ui"; import LicenseRequired from "../../common/components/v2/LicenseRequired"; import SkeletonLoader from "../components/SkeletonLoaderList"; -import WorkflowList, { WorkflowType } from "../components/WorkflowListPage"; +import type { WorkflowType } from "../components/WorkflowListPage"; +import WorkflowList from "../components/WorkflowListPage"; function WorkflowsPage() { const { t } = useLocale(); @@ -78,21 +80,24 @@ function WorkflowsPage() { if (!query.data) return null; + const profileOptions = query.data.profiles.map((profile) => { + return { teamId: profile.teamId, label: profile.name || profile.slug, image: profile.image }; + }); + return ( 0 ? ( { - console.log("test test"); createMutation.mutate({ teamId }); }} isLoading={createMutation.isLoading} @@ -107,7 +112,7 @@ function WorkflowsPage() { ) : ( <> - {query.data.profiles.length > 1 && + {profileOptions.length > 1 && allWorkflowsData?.workflows && allWorkflowsData.workflows.length > 0 && (
    @@ -119,7 +124,7 @@ function WorkflowsPage() {
    createMutation.mutate({ teamId })} isLoading={createMutation.isLoading} disableMobileButton={true} @@ -129,7 +134,7 @@ function WorkflowsPage() { )} diff --git a/packages/trpc/server/routers/viewer/eventTypes.ts b/packages/trpc/server/routers/viewer/eventTypes.ts index a8b8081105242b..c67c7b61739b42 100644 --- a/packages/trpc/server/routers/viewer/eventTypes.ts +++ b/packages/trpc/server/routers/viewer/eventTypes.ts @@ -219,6 +219,7 @@ export const eventTypesRouter = router({ startTime: true, endTime: true, bufferTime: true, + avatar: true, teams: { where: { accepted: true, @@ -323,6 +324,7 @@ export const eventTypesRouter = router({ profile: { slug: user.username, name: user.name, + image: user.avatar || undefined, }, eventTypes: _.orderBy(mergedEventTypes, ["position", "id"], ["desc", "asc"]), metadata: { diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index 150cd610eca39c..cd2ac34f111bfc 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -1,5 +1,5 @@ +import type { PrismaPromise, Prisma, PrismaClient, Workflow } from "@prisma/client"; import { - PrismaPromise, WorkflowTemplates, WorkflowActions, WorkflowTriggerEvents, @@ -7,9 +7,6 @@ import { WorkflowMethods, TimeUnit, MembershipRole, - Prisma, - PrismaClient, - Workflow } from "@prisma/client"; import { z } from "zod"; @@ -34,7 +31,7 @@ import { verifyPhoneNumber, sendVerificationCode, } from "@calcom/features/ee/workflows/lib/reminders/verifyPhoneNumber"; -import { IS_SELF_HOSTED, SENDER_ID } from "@calcom/lib/constants"; +import { IS_SELF_HOSTED, SENDER_ID, CAL_URL } from "@calcom/lib/constants"; import { SENDER_NAME } from "@calcom/lib/constants"; // import { getErrorFromUnknown } from "@calcom/lib/errors"; import { getTranslation } from "@calcom/lib/server/i18n"; @@ -564,11 +561,11 @@ export const workflowsRouter = router({ }), organizer: booking.user ? { - language: { locale: booking.user.locale || "" }, - name: booking.user.name || "", - email: booking.user.email, - timeZone: booking.user.timeZone, - } + language: { locale: booking.user.locale || "" }, + name: booking.user.name || "", + email: booking.user.email, + timeZone: booking.user.timeZone, + } : { name: "", email: "", timeZone: "", language: { locale: "" } }, startTime: booking.startTime.toISOString(), endTime: booking.endTime.toISOString(), @@ -756,11 +753,11 @@ export const workflowsRouter = router({ }), organizer: booking.user ? { - language: { locale: booking.user.locale || "" }, - name: booking.user.name || "", - email: booking.user.email, - timeZone: booking.user.timeZone, - } + language: { locale: booking.user.locale || "" }, + name: booking.user.name || "", + email: booking.user.email, + timeZone: booking.user.timeZone, + } : { name: "", email: "", timeZone: "", language: { locale: "" } }, startTime: booking.startTime.toISOString(), endTime: booking.endTime.toISOString(), @@ -883,11 +880,11 @@ export const workflowsRouter = router({ }), organizer: booking.user ? { - name: booking.user.name || "", - email: booking.user.email, - timeZone: booking.user.timeZone, - language: { locale: booking.user.locale || "" }, - } + name: booking.user.name || "", + email: booking.user.email, + timeZone: booking.user.timeZone, + language: { locale: booking.user.locale || "" }, + } : { name: "", email: "", timeZone: "", language: { locale: "" } }, startTime: booking.startTime.toISOString(), endTime: booking.endTime.toISOString(), @@ -1289,6 +1286,7 @@ action === WorkflowActions.EMAIL_ADDRESS*/ select: { id: true, username: true, + avatar: true, name: true, startTime: true, endTime: true, @@ -1339,6 +1337,7 @@ action === WorkflowActions.EMAIL_ADDRESS*/ profile: { slug: (typeof user)["username"]; name: (typeof user)["name"]; + image?: string; }; metadata?: { readOnly: boolean; @@ -1353,6 +1352,7 @@ action === WorkflowActions.EMAIL_ADDRESS*/ profile: { slug: user.username, name: user.name, + image: user.avatar || undefined, }, workflows: userWorkflows, metadata: { @@ -1367,6 +1367,7 @@ action === WorkflowActions.EMAIL_ADDRESS*/ profile: { name: membership.team.name, slug: "team/" + membership.team.slug, + image: `${CAL_URL}/team/${membership.team.slug}/avatar.png`, }, metadata: { readOnly: membership.role === MembershipRole.MEMBER, diff --git a/packages/ui/components/createButton/CreateButton.tsx b/packages/ui/components/createButton/CreateButton.tsx index 97d616f0b8546c..2e19783b217345 100644 --- a/packages/ui/components/createButton/CreateButton.tsx +++ b/packages/ui/components/createButton/CreateButton.tsx @@ -14,15 +14,14 @@ import { } from "@calcom/ui"; import { FiPlus } from "@calcom/ui/components/icon"; -export interface Parent { +export interface Option { teamId: number | null | undefined; // if undefined, then it's a profile - name?: string | null; - slug?: string | null; + label: string | null; image?: string | null; } interface CreateBtnProps { - options: Parent[]; + options: Option[]; createDialog?: () => JSX.Element; duplicateDialog?: () => JSX.Element; createFunction?: (teamId?: number) => void; @@ -42,11 +41,11 @@ export function CreateButton(props: CreateBtnProps) { const hasTeams = !!props.options.find((option) => option.teamId); // inject selection data into url for correct router history - const openModal = (option: Parent) => { + const openModal = (option: Option) => { const query = { ...router.query, dialog: "new", - eventPage: option.slug, + eventPage: option.label, teamId: option.teamId, }; if (!option.teamId) { @@ -94,13 +93,13 @@ export function CreateButton(props: CreateBtnProps) {
    {props.subtitle}
    {props.options.map((option) => ( - + ( @@ -114,7 +113,7 @@ export function CreateButton(props: CreateBtnProps) { }> {" "} {/*improve this code */} - {option.name ? option.name : option.slug} + {option.label} ))} From 9b77f89af1e20cbe4b45d9ce3b8e0ff3fe1847e4 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Tue, 21 Feb 2023 11:23:47 -0500 Subject: [PATCH 54/64] make workflows read-only for members --- apps/web/pages/event-types/index.tsx | 8 ++- .../ee/workflows/components/DeleteDialog.tsx | 6 +- .../components/EventWorkflowsTab.tsx | 6 +- .../workflows/components/WorkflowListPage.tsx | 23 ++++++- .../features/ee/workflows/pages/index.tsx | 12 ++-- .../features/ee/workflows/pages/workflow.tsx | 11 ++- .../trpc/server/routers/viewer/workflows.tsx | 69 +++++++++++++++++-- 7 files changed, 114 insertions(+), 21 deletions(-) diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index 003fb2faabd72c..6c3bc8846c05fd 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -636,9 +636,11 @@ const CTA = () => { if (!query.data) return null; - const profileOptions = query.data.profiles.map((profile) => { - return { teamId: profile.teamId, label: profile.name || profile.slug, image: profile.image }; - }); + const profileOptions = query.data.profiles + .filter((profile) => !profile.readOnly) + .map((profile) => { + return { teamId: profile.teamId, label: profile.name || profile.slug, image: profile.image }; + }); return ( { 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"); + } }, }); diff --git a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx index 8bba40a9a21d3f..e80f198f261d71 100644 --- a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx +++ b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx @@ -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,10 @@ const WorkflowListItem = (props: ItemProps) => { const message = `${err.statusCode}: ${err.message}`; showToast(message, "error"); } + if (err.data?.code === "UNAUTHORIZED") { + const message = `${err.data.code}: You are not authorized to enable or disable this workflow`; + showToast(message, "error"); + } }, }); diff --git a/packages/features/ee/workflows/components/WorkflowListPage.tsx b/packages/features/ee/workflows/components/WorkflowListPage.tsx index 2d65220f829ed5..eabe7aef09fd1c 100644 --- a/packages/features/ee/workflows/components/WorkflowListPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowListPage.tsx @@ -1,4 +1,6 @@ -import type { Workflow, WorkflowStep } from "@prisma/client"; +import type { Workflow, WorkflowStep, Membership } from "@prisma/client"; +import { MembershipRole } from "@prisma/client"; +import { useSession } from "next-auth/react"; import Link from "next/link"; import { useRouter } from "next/router"; import { useState } from "react"; @@ -26,8 +28,9 @@ import EmptyScreen from "./EmptyScreen"; export type WorkflowType = Workflow & { team: { id: number; + name: string; + members: Membership[]; slug: string | null; - name?: string | null; } | null; steps: WorkflowStep[]; activeOn: { @@ -36,6 +39,7 @@ export type WorkflowType = Workflow & { title: string; }; }[]; + readOnly?: boolean; }; interface Props { workflows: WorkflowType[] | undefined; @@ -52,6 +56,7 @@ export default function WorkflowListPage({ workflows, profileOptions, hasNoWorkf const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [workflowToDeleteId, setwWorkflowToDeleteId] = useState(0); const router = useRouter(); + const session = useSession(); return ( <> @@ -133,6 +138,13 @@ export default function WorkflowListPage({ workflows, profileOptions, hasNoWorkf type="button" color="secondary" variant="icon" + disabled={ + !!workflow.team?.members?.find( + (member) => + member.userId === session.data?.user.id && + member.role === MembershipRole.MEMBER + ) + } StartIcon={FiEdit2} onClick={async () => await router.replace("/workflows/" + workflow.id)} /> @@ -144,6 +156,13 @@ export default function WorkflowListPage({ workflows, profileOptions, hasNoWorkf setwWorkflowToDeleteId(workflow.id); }} color="secondary" + disabled={ + !!workflow.team?.members?.find( + (member) => + member.userId === session.data?.user.id && + member.role === MembershipRole.MEMBER + ) + } variant="icon" StartIcon={FiTrash2} /> diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index 05b969137dcc31..55c95eff844f65 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -80,9 +80,11 @@ function WorkflowsPage() { if (!query.data) return null; - const profileOptions = query.data.profiles.map((profile) => { - return { teamId: profile.teamId, label: profile.name || profile.slug, image: profile.image }; - }); + const profileOptions = query.data.profiles + .filter((profile) => !profile.readOnly) + .map((profile) => { + return { teamId: profile.teamId, label: profile.name || profile.slug, image: profile.image }; + }); return ( 0 ? ( @@ -112,7 +114,7 @@ function WorkflowsPage() { ) : ( <> - {profileOptions.length > 1 && + {query.data.profiles.length > 1 && allWorkflowsData?.workflows && allWorkflowsData.workflows.length > 0 && (
    diff --git a/packages/features/ee/workflows/pages/workflow.tsx b/packages/features/ee/workflows/pages/workflow.tsx index 14af0fb04b7b9a..0caee1b9cadd16 100644 --- a/packages/features/ee/workflows/pages/workflow.tsx +++ b/packages/features/ee/workflows/pages/workflow.tsx @@ -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"; @@ -116,6 +117,10 @@ function WorkflowPage() { } ); + 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)) { @@ -256,7 +261,9 @@ function WorkflowPage() { title={workflow && workflow.name ? workflow.name : "Untitled"} CTA={
    - +
    } heading={ diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index cd2ac34f111bfc..dd6e47be6d543e 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -55,12 +55,35 @@ async function isAuthorized( never, Prisma.RejectOnNotFound | Prisma.RejectPerOperation | undefined >, - currentUserId: number + currentUserId: number, + readOnly?: boolean ) { if (!workflow) { return false; } + if (!readOnly) { + const userWorkflow = await prisma.workflow.findFirst({ + where: { + id: workflow.id, + OR: [ + { userId: currentUserId }, + { + team: { + members: { + some: { + userId: currentUserId, + accepted: true, + }, + }, + }, + }, + ], + }, + }); + if (userWorkflow) return true; + } + const userWorkflow = await prisma.workflow.findFirst({ where: { id: workflow.id, @@ -72,6 +95,9 @@ async function isAuthorized( some: { userId: currentUserId, accepted: true, + NOT: { + role: MembershipRole.MEMBER, + }, }, }, }, @@ -110,6 +136,14 @@ export const workflowsRouter = router({ }, }, include: { + team: { + select: { + id: true, + slug: true, + name: true, + members: true, + }, + }, activeOn: { select: { eventType: { @@ -121,7 +155,6 @@ export const workflowsRouter = router({ }, }, steps: true, - team: true, }, orderBy: { id: "asc", @@ -148,7 +181,14 @@ export const workflowsRouter = router({ }, }, steps: true, - team: true, + team: { + select: { + id: true, + slug: true, + name: true, + members: true, + }, + }, }, orderBy: { id: "asc", @@ -185,7 +225,14 @@ export const workflowsRouter = router({ }, }, steps: true, - team: true, + team: { + select: { + id: true, + slug: true, + name: true, + members: true, + }, + }, }, orderBy: { id: "asc", @@ -214,6 +261,7 @@ export const workflowsRouter = router({ select: { id: true, slug: true, + members: true, }, }, time: true, @@ -261,6 +309,9 @@ export const workflowsRouter = router({ some: { userId: ctx.user.id, accepted: true, + NOT: { + role: MembershipRole.MEMBER, + }, }, }, }, @@ -315,7 +366,7 @@ export const workflowsRouter = router({ }, }); - const isUserAuthorized = await isAuthorized(workflowToDelete, ctx.prisma, ctx.user.id); + const isUserAuthorized = await isAuthorized(workflowToDelete, ctx.prisma, ctx.user.id, true); if (!isUserAuthorized || !workflowToDelete) { throw new TRPCError({ code: "UNAUTHORIZED" }); @@ -399,7 +450,7 @@ export const workflowsRouter = router({ }, }); - const isUserAuthorized = await isAuthorized(userWorkflow, ctx.prisma, ctx.user.id); + const isUserAuthorized = await isAuthorized(userWorkflow, ctx.prisma, ctx.user.id, true); if (!isUserAuthorized || !userWorkflow) { throw new TRPCError({ code: "UNAUTHORIZED" }); @@ -976,6 +1027,7 @@ export const workflowsRouter = router({ select: { id: true, slug: true, + members: true, }, }, steps: { @@ -1163,7 +1215,7 @@ action === WorkflowActions.EMAIL_ADDRESS*/ .mutation(async ({ ctx, input }) => { const { eventTypeId, workflowId } = input; - // Check that workflow & event type belong to the user or team + // Check that vent type belong to the user or team const userEventType = await ctx.prisma.eventType.findFirst({ where: { id: eventTypeId, @@ -1175,6 +1227,9 @@ action === WorkflowActions.EMAIL_ADDRESS*/ some: { userId: ctx.user.id, accepted: true, + NOT: { + role: MembershipRole.MEMBER, + }, }, }, }, From a47161d8c53c64a69e5bb03d369250203f779c78 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Tue, 21 Feb 2023 11:35:52 -0500 Subject: [PATCH 55/64] remove duplicateDialog from createButton component --- apps/web/pages/event-types/index.tsx | 3 ++- packages/ui/components/createButton/CreateButton.tsx | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index 6c3bc8846c05fd..3c224a79a02c7e 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -647,7 +647,6 @@ const CTA = () => { subtitle={t("create_event_on").toUpperCase()} options={profileOptions} createDialog={CreateEventTypeDialog} - duplicateDialog={DuplicateDialog} /> ); }; @@ -656,6 +655,7 @@ const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer); const EventTypesPage = () => { const { t } = useLocale(); + const router = useRouter(); const isMobile = useMediaQuery("(max-width: 768px)"); @@ -701,6 +701,7 @@ const EventTypesPage = () => { )} + {router.query.dialog === "duplicate" && } )} /> diff --git a/packages/ui/components/createButton/CreateButton.tsx b/packages/ui/components/createButton/CreateButton.tsx index 2e19783b217345..192e6d31537f05 100644 --- a/packages/ui/components/createButton/CreateButton.tsx +++ b/packages/ui/components/createButton/CreateButton.tsx @@ -23,7 +23,6 @@ export interface Option { interface CreateBtnProps { options: Option[]; createDialog?: () => JSX.Element; - duplicateDialog?: () => JSX.Element; createFunction?: (teamId?: number) => void; subtitle?: string; buttonText?: string; @@ -36,7 +35,6 @@ export function CreateButton(props: CreateBtnProps) { const router = useRouter(); const CreateDialog = props.createDialog ? props.createDialog() : null; - const DuplicateDialog = props.duplicateDialog ? props.duplicateDialog() : null; const hasTeams = !!props.options.find((option) => option.teamId); @@ -120,7 +118,6 @@ export function CreateButton(props: CreateBtnProps) { )} - {router.query.dialog === "duplicate" && DuplicateDialog} {router.query.dialog === "new" && CreateDialog} ); From 597af9d4152314778decb3864939dbd3bbe4ffc3 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Tue, 21 Feb 2023 11:46:46 -0500 Subject: [PATCH 56/64] improve types --- packages/trpc/server/routers/viewer/workflows.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx index dd6e47be6d543e..6e05e3ad31842f 100644 --- a/packages/trpc/server/routers/viewer/workflows.tsx +++ b/packages/trpc/server/routers/viewer/workflows.tsx @@ -1,4 +1,4 @@ -import type { PrismaPromise, Prisma, PrismaClient, Workflow } from "@prisma/client"; +import type { PrismaPromise, Workflow } from "@prisma/client"; import { WorkflowTemplates, WorkflowActions, @@ -35,6 +35,7 @@ import { IS_SELF_HOSTED, SENDER_ID, CAL_URL } from "@calcom/lib/constants"; import { SENDER_NAME } from "@calcom/lib/constants"; // import { getErrorFromUnknown } from "@calcom/lib/errors"; import { getTranslation } from "@calcom/lib/server/i18n"; +import type PrismaType from "@calcom/prisma"; import type { WorkflowStep } from "@calcom/prisma/client"; import { TRPCError } from "@trpc/server"; @@ -50,11 +51,7 @@ function getSender( async function isAuthorized( workflow: Pick | null, - prisma: PrismaClient< - Prisma.PrismaClientOptions, - never, - Prisma.RejectOnNotFound | Prisma.RejectPerOperation | undefined - >, + prisma: typeof PrismaType, currentUserId: number, readOnly?: boolean ) { From 2983e6ce6d551a9dd6b06d32a4bbc4e0088f8e38 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Tue, 21 Feb 2023 12:04:55 -0500 Subject: [PATCH 57/64] fix type error --- packages/lib/getEventTypeById.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lib/getEventTypeById.ts b/packages/lib/getEventTypeById.ts index cddc724910b9ea..b396caa2efc2d6 100644 --- a/packages/lib/getEventTypeById.ts +++ b/packages/lib/getEventTypeById.ts @@ -157,6 +157,7 @@ export default async function getEventTypeById({ id: true, slug: true, name: true, + members: true, }, }, activeOn: { From 420d3efd4f17c24d5bde56640f54f2f4b1eefedc Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Thu, 23 Feb 2023 14:03:21 -0500 Subject: [PATCH 58/64] fix default value for location in create event type dialog --- .../components/CreateEventTypeDialog.tsx | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/features/eventtypes/components/CreateEventTypeDialog.tsx b/packages/features/eventtypes/components/CreateEventTypeDialog.tsx index 351a1b7355f351..fe16070ecb5884 100644 --- a/packages/features/eventtypes/components/CreateEventTypeDialog.tsx +++ b/packages/features/eventtypes/components/CreateEventTypeDialog.tsx @@ -23,9 +23,6 @@ import { TextAreaField, TextField, } from "@calcom/ui"; -import { FiPlus } from "@calcom/ui/components/icon"; - -import { DuplicateDialog } from "./DuplicateDialog"; // this describes the uniform data needed to create a new event type on Profile or Team export interface EventTypeParent { @@ -53,10 +50,7 @@ const querySchema = z.object({ teamId: z.union([z.string().transform((val) => +val), z.number()]).optional(), title: z.string().optional(), slug: z.string().optional(), - length: z - .union([z.string().transform((val) => +val), z.number()]) - .optional() - .default(15), + length: z.union([z.string().transform((val) => +val), z.number()]).optional(), description: z.string().optional(), schedulingType: z.nativeEnum(SchedulingType).optional(), locations: z @@ -70,12 +64,14 @@ export default function CreateEventTypeDialog() { const router = useRouter(); const { - data: { teamId, eventPage: pageSlug, ...defaultValues }, + data: { teamId, eventPage: pageSlug }, } = useTypedQuery(querySchema); const form = useForm>({ + defaultValues: { + length: 15, + }, resolver: zodResolver(createEventTypeInput), - defaultValues, }); const { register } = form; @@ -202,26 +198,26 @@ export default function CreateEventTypeDialog() { message={form.formState.errors.schedulingType.message} /> )} - + - {t("collective")} + {t("collective")}

    {t("collective_description")}

    - {t("round_robin")} + {t("round_robin")}

    {t("round_robin_description")}

    )}
    -
    +
    From 08fb190710ba339f79fae912ad1748924e95c1f1 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Thu, 23 Feb 2023 14:24:18 -0500 Subject: [PATCH 59/64] remove duplicate verification input --- apps/web/public/static/locales/en/common.json | 4 +- .../components/WorkflowStepContainer.tsx | 110 ++++++------------ 2 files changed, 41 insertions(+), 73 deletions(-) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index a1136ccc8c5fe5..6b5d84dd224212 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1622,5 +1622,7 @@ "select_a_router": "Select a router", "add_a_new_route": "Add a new Route", "no_responses_yet": "No responses yet", - "this_will_be_the_placeholder": "This will be the placeholder" + "this_will_be_the_placeholder": "This will be the placeholder", + "verification_code": "Verification code", + "verify": "Verify" } diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index 88fd9475f7b48e..613bb23e8492f1 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -1,12 +1,9 @@ -import { - TimeUnit, - WorkflowActions, - WorkflowStep, - WorkflowTemplates, - WorkflowTriggerEvents, -} from "@prisma/client"; -import { Dispatch, SetStateAction, useRef, useState, useEffect } 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"; @@ -209,7 +206,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { return ( <>
    -
    +
    1 @@ -219,7 +216,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
    {t("when_something_happens")}
    -
    +
    -
    +
    -
    +
    @@ -335,7 +332,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
    -
    +
    {isPhoneNumberNeeded && ( -
    +
    @@ -442,7 +439,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
    ) : ( <> -
    +
    - Verify + {t("verify")}
    {form.formState.errors.steps && @@ -471,43 +469,11 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { {form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}

    )} - {numberVerified ? ( -
    - {t("number_verified")} -
    - ) : ( - <> -
    - { - setVerificationCode(e.target.value); - }} - required - /> - -
    - - )} )}
    )} -
    +
    {isSenderIdNeeded ? ( <>
    @@ -557,7 +523,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
    )} {isEmailAddressNeeded && ( -
    +
    {isCustomReminderBodyNeeded && ( -
    +
    {isEmailSubjectNeeded && (
    - +
    -
    @@ -644,7 +610,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { ) : ( <>
    -
    + )}
    ))} From 89efc741715e09519887fef195e2012c7a6b63c8 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Thu, 23 Feb 2023 15:41:43 -0500 Subject: [PATCH 62/64] improve readonly UI --- .../workflows/components/WorkflowListPage.tsx | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/packages/features/ee/workflows/components/WorkflowListPage.tsx b/packages/features/ee/workflows/components/WorkflowListPage.tsx index 1f222f20dd59e1..92201ac0aef06f 100644 --- a/packages/features/ee/workflows/components/WorkflowListPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowListPage.tsx @@ -86,7 +86,7 @@ export default function WorkflowListPage({ workflows, profileOptions, hasNoWorkf
    {workflow.readOnly && ( - + {t("readonly")} )} @@ -139,32 +139,35 @@ export default function WorkflowListPage({ workflows, profileOptions, hasNoWorkf
    - {!workflow.readOnly && ( -
    -
    - - -
    + +
    +
    + + +
    + {!workflow.readOnly && (
    @@ -199,8 +202,8 @@ export default function WorkflowListPage({ workflows, profileOptions, hasNoWorkf
    -
    - )} + )} +
    ))} From a987482a03ae2a634d4f97249cc39ac6091ee32a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Thu, 23 Feb 2023 14:31:42 -0700 Subject: [PATCH 63/64] Update packages/features/ee/workflows/components/EventWorkflowsTab.tsx --- packages/features/ee/workflows/components/EventWorkflowsTab.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx index e80f198f261d71..8f1189f5c03d55 100644 --- a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx +++ b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx @@ -66,6 +66,7 @@ const WorkflowListItem = (props: ItemProps) => { 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"); } From c7803e1b418d89da316cc80d157b97f36dc8fdc1 Mon Sep 17 00:00:00 2001 From: zomars Date: Thu, 23 Feb 2023 14:40:03 -0700 Subject: [PATCH 64/64] Update WorkflowDetailsPage.tsx --- .../components/WorkflowDetailsPage.tsx | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx index c7521980236252..38d77470671c19 100644 --- a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx @@ -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"; @@ -38,25 +41,17 @@ export default function WorkflowDetailsPage(props: Props) { const eventTypeOptions = useMemo( () => - data?.eventTypeGroups - .filter((eventType) => { - if (!teamId && !eventType.teamId) { - return eventType; - } - if (teamId === eventType.teamId) { - return eventType; - } - }) - .reduce( - (options, group) => [ - ...options, - ...group.eventTypes.map((eventType) => ({ - value: String(eventType.id), - label: eventType.title, - })), - ], - [] as Option[] - ) || [], + 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[]) || [], [data] );