feat: disable workflows and webhooks for orgs + UI changes [CAL-1873] (#9504)

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Efraín Rochín <roae.85@gmail.com>
This commit is contained in:
Carina Wollendorfer
2023-06-26 06:37:59 -04:00
committed by GitHub
co-authored by sean-brydon Leo Giovanetti Joe Au-Yeung Udit Takkar Hariom Balhara Udit Takkar CarinaWolli zomars Efraín Rochín
parent b3d64104b0
commit 78393a2b8b
20 changed files with 362 additions and 381 deletions
@@ -1,6 +1,5 @@
import classNames from "classnames";
import { signIn } from "next-auth/react";
import { useState } from "react";
import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -231,7 +230,7 @@ export default function MemberListItem(props: Props) {
<DropdownMenuItem className="outline-none">
<DropdownItem
disabled={!props.member.accepted}
href={!props.member.accepted?undefined : "/" + props.member.username}
href={!props.member.accepted ? undefined : "/" + props.member.username}
target="_blank"
type="button"
StartIcon={ExternalLink}>
@@ -328,40 +328,40 @@ const ProfileView = () => {
</Dialog>
)}
</>
):(
) : (
<>
<SkeletonContainer as="form">
<div className="flex items-center">
<div className="ms-4">
<SkeletonContainer>
<div className="bg-emphasis rounded-full w-16 h-16" />
<div className="bg-emphasis h-16 w-16 rounded-full" />
</SkeletonContainer>
</div>
</div>
<hr className="border-subtle my-8" />
<SkeletonContainer>
<div className="mt-8">
<SkeletonText className="w-48 h-6" />
<SkeletonText className="h-6 w-48" />
</div>
</SkeletonContainer>
<SkeletonContainer>
<div className="mt-8">
<SkeletonText className="w-48 h-6" />
<SkeletonText className="h-6 w-48" />
</div>
</SkeletonContainer>
<div className="mt-8">
<SkeletonContainer>
<div className="h-24 bg-emphasis rounded-md" />
<div className="bg-emphasis h-24 rounded-md" />
</SkeletonContainer>
<SkeletonText className="w-32 h-12 mt-4" />
<SkeletonText className="mt-4 h-12 w-32" />
</div>
<SkeletonContainer>
<div className="mt-8">
<SkeletonText className="w-24 h-9" />
<SkeletonText className="h-9 w-24" />
</div>
</SkeletonContainer>
</SkeletonContainer>
</>
</SkeletonContainer>
</>
)}
</>
);
@@ -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);
@@ -4,7 +4,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { trpc } from "@calcom/trpc/react";
import type { SVGComponent } from "@calcom/types/SVGComponent";
import { CreateButton, showToast, EmptyScreen as ClassicEmptyScreen } from "@calcom/ui";
import { showToast, EmptyScreen as ClassicEmptyScreen, CreateButtonWithTeamsList } from "@calcom/ui";
import { Smartphone, Mail, Zap } from "@calcom/ui/components/icon";
type WorkflowExampleType = {
@@ -31,15 +31,7 @@ function WorkflowExample(props: WorkflowExampleType) {
);
}
export default function EmptyScreen(props: {
profileOptions: {
label: string | null;
image?: string | null;
teamId: number | null | undefined;
slug: string | null;
}[];
isFilteredView: boolean;
}) {
export default function EmptyScreen(props: { isFilteredView: boolean }) {
const { t } = useLocale();
const router = useRouter();
@@ -86,9 +78,8 @@ export default function EmptyScreen(props: {
{t("no_workflows_description")}
</p>
<div className="mt-8 ">
<CreateButton
<CreateButtonWithTeamsList
subtitle={t("new_workflow_subtitle").toUpperCase()}
options={props.profileOptions}
createFunction={(teamId?: number) => createMutation.mutate({ teamId })}
buttonText={t("create_workflow")}
isLoading={createMutation.isLoading}
@@ -4,6 +4,8 @@ import { useRouter } from "next/router";
import { useState } from "react";
import classNames from "@calcom/lib/classNames";
import { CAL_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
@@ -16,12 +18,14 @@ import {
DropdownMenuTrigger,
Tooltip,
Badge,
Avatar,
} from "@calcom/ui";
import { Edit2, Link as LinkIcon, MoreHorizontal, Trash2 } from "@calcom/ui/components/icon";
import { useOrgBrandingValues } from "../../organizations/hooks";
import { subdomainSuffix } from "../../organizations/lib/orgDomains";
import { getActionIcon } from "../lib/getActionIcon";
import { DeleteDialog } from "./DeleteDialog";
import EmptyScreen from "./EmptyScreen";
export type WorkflowType = Workflow & {
team: {
@@ -29,6 +33,7 @@ export type WorkflowType = Workflow & {
name: string;
members: Membership[];
slug: string | null;
logo?: string | null;
} | null;
steps: WorkflowStep[];
activeOn: {
@@ -45,21 +50,17 @@ export type WorkflowType = Workflow & {
};
interface Props {
workflows: WorkflowType[] | undefined;
profileOptions: {
image?: string | null;
label: string | null;
teamId: number | null | undefined;
slug: string | null;
}[];
hasNoWorkflows?: boolean;
}
export default function WorkflowListPage({ workflows, profileOptions, hasNoWorkflows }: Props) {
export default function WorkflowListPage({ workflows }: Props) {
const { t } = useLocale();
const utils = trpc.useContext();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [workflowToDeleteId, setwWorkflowToDeleteId] = useState(0);
const router = useRouter();
const orgBranding = useOrgBrandingValues();
const urlPrefix = orgBranding ? `${orgBranding.slug}.${subdomainSuffix()}` : CAL_URL;
return (
<>
{workflows && workflows.length > 0 ? (
@@ -143,16 +144,41 @@ export default function WorkflowListPage({ workflows, profileOptions, hasNoWorkf
)}
</Badge>
</li>
{workflow.teamId && (
<li>
<Badge variant="gray">
<>{workflow.team?.name}</>
</Badge>
</li>
)}
<div className="block md:hidden">
{workflow.team?.name && (
<li>
<Badge variant="gray">
<>{workflow.team.name}</>
</Badge>
</li>
)}
</div>
</ul>
</div>
</Link>
<div>
<div className="hidden md:block">
{workflow.team?.name && (
<Badge className="mr-4 mt-1 p-[1px] px-2" variant="gray">
<Avatar
alt={workflow.team?.name || ""}
href={
workflow.team?.id
? `/settings/teams/${workflow.team?.id}/profile`
: "/settings/my-account/profile"
}
imageSrc={getPlaceholderAvatar(
workflow?.team.logo,
workflow.team?.name as string
)}
size="xxs"
className="mt-[3px] inline-flex justify-center"
/>
<div>{workflow.team.name}</div>
</Badge>
)}
</div>
</div>
<div className="flex flex-shrink-0">
<div className="hidden sm:block">
@@ -222,12 +248,12 @@ export default function WorkflowListPage({ workflows, profileOptions, hasNoWorkf
setIsOpenDialog={setDeleteDialogOpen}
workflowId={workflowToDeleteId}
additionalFunction={async () => {
await utils.viewer.workflows.list.invalidate();
await utils.viewer.workflows.filteredList.invalidate();
}}
/>
</div>
) : (
<EmptyScreen profileOptions={profileOptions} isFilteredView={!hasNoWorkflows} />
<></>
)}
</>
);
+34 -89
View File
@@ -1,7 +1,7 @@
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import type { Dispatch, SetStateAction } from "react";
import { useState, useEffect } from "react";
import { useState } from "react";
import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib";
@@ -9,11 +9,14 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { trpc } from "@calcom/trpc/react";
import { AnimatedPopover, Avatar, CreateButton, showToast } from "@calcom/ui";
import { AnimatedPopover, Avatar, showToast, CreateButtonWithTeamsList } from "@calcom/ui";
import { FilterResults } from "../../../filters/components/FilterResults";
import { TeamsFilter } from "../../../filters/components/TeamsFilter";
import { getTeamsFiltersFromQuery } from "../../../filters/lib/getTeamsFiltersFromQuery";
import LicenseRequired from "../../common/components/LicenseRequired";
import EmptyScreen from "../components/EmptyScreen";
import SkeletonLoader from "../components/SkeletonLoaderList";
import type { WorkflowType } from "../components/WorkflowListPage";
import WorkflowList from "../components/WorkflowListPage";
function WorkflowsPage() {
@@ -25,9 +28,11 @@ function WorkflowsPage() {
teamIds: [],
});
const { data: allWorkflowsData, isLoading } = trpc.viewer.workflows.list.useQuery();
const filters = getTeamsFiltersFromQuery(router.query);
const [filteredWorkflows, setFilteredWorkflows] = useState<WorkflowType[]>([]);
const queryRes = trpc.viewer.workflows.filteredList.useQuery({
filters,
});
const createMutation = trpc.viewer.workflows.create.useMutation({
onSuccess: async ({ workflow }) => {
@@ -46,50 +51,6 @@ function WorkflowsPage() {
},
});
const query = trpc.viewer.workflows.getByViewer.useQuery();
useEffect(() => {
const allWorkflows = allWorkflowsData?.workflows;
if (allWorkflows && allWorkflows.length > 0) {
const filtered = allWorkflows.filter((workflow) => {
if (checkedFilterItems.teamIds.includes(workflow.teamId || 0)) return workflow;
if (!workflow.teamId) {
if (!!workflow.userId && workflow.userId === checkedFilterItems.userId) return workflow;
}
});
setFilteredWorkflows(filtered);
} else {
setFilteredWorkflows([]);
}
}, [checkedFilterItems, allWorkflowsData]);
useEffect(() => {
if (session.status !== "loading" && !query.isLoading) {
if (!query.data) return;
setCheckedFilterItems({
userId: session.data?.user.id || null,
teamIds: query.data.profiles
.map((profile) => {
if (!!profile.teamId) {
return profile.teamId;
}
})
.filter((teamId): teamId is number => !!teamId),
});
}
}, [session.status, query.isLoading, allWorkflowsData]);
const profileOptions = query?.data?.profiles
.filter((profile) => !profile.readOnly)
.map((profile) => {
return {
teamId: profile.teamId,
label: profile.name || profile.slug,
image: profile.image,
slug: profile.slug,
};
});
return (
<Shell
heading={t("workflows")}
@@ -97,58 +58,42 @@ function WorkflowsPage() {
subtitle={t("workflows_to_automate_notifications")}
hideHeadingOnMobile
CTA={
query?.data?.profiles.length === 1 &&
session.data?.hasValidLicense &&
allWorkflowsData?.workflows &&
allWorkflowsData?.workflows.length &&
profileOptions ? (
<CreateButton
session.data?.hasValidLicense ? (
<CreateButtonWithTeamsList
subtitle={t("new_workflow_subtitle").toUpperCase()}
options={profileOptions}
createFunction={(teamId?: number) => {
createMutation.mutate({ teamId });
}}
isLoading={createMutation.isLoading}
disableMobileButton={true}
onlyShowWithNoTeams={true}
/>
) : null
}>
<LicenseRequired>
{isLoading ? (
<SkeletonLoader />
) : (
<>
{query?.data?.profiles &&
query?.data?.profiles.length > 1 &&
allWorkflowsData?.workflows &&
allWorkflowsData.workflows.length &&
profileOptions ? (
<div className="mb-4 flex">
<Filter
profiles={query.data.profiles}
checked={checkedFilterItems}
setChecked={setCheckedFilterItems}
<>
{queryRes.data?.totalCount ? (
<div className="flex">
<TeamsFilter />
<div className="ml-auto">
<CreateButtonWithTeamsList
subtitle={t("new_workflow_subtitle").toUpperCase()}
createFunction={(teamId?: number) => createMutation.mutate({ teamId })}
isLoading={createMutation.isLoading}
disableMobileButton={true}
onlyShowWithTeams={true}
/>
<div className="ml-auto">
<CreateButton
subtitle={t("new_workflow_subtitle").toUpperCase()}
options={profileOptions}
createFunction={(teamId?: number) => createMutation.mutate({ teamId })}
isLoading={createMutation.isLoading}
disableMobileButton={true}
/>
</div>
</div>
) : null}
{profileOptions && profileOptions?.length ? (
<WorkflowList
workflows={filteredWorkflows}
profileOptions={profileOptions}
hasNoWorkflows={!allWorkflowsData?.workflows || allWorkflowsData?.workflows.length === 0}
/>
) : null}
</>
)}
</div>
) : null}
<FilterResults
queryRes={queryRes}
emptyScreen={<EmptyScreen isFilteredView={false} />}
noResultsScreen={<EmptyScreen isFilteredView={true} />}
SkeletonLoader={SkeletonLoader}>
<WorkflowList workflows={queryRes.data?.filtered} />
</FilterResults>
</>
</LicenseRequired>
</Shell>
);
@@ -271,7 +271,7 @@ function WorkflowPage() {
</div>
{workflow && workflow.team && (
<Badge className="ml-4 mt-1" variant="gray">
{workflow.team.slug}
{workflow.team.name}
</Badge>
)}
{readOnly && (
@@ -5,6 +5,7 @@ import { forwardRef } from "react";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
import { AnimatedPopover, Avatar, Divider, VerticalDivider } from "@calcom/ui";
@@ -84,28 +85,30 @@ export const TeamsFilter = ({
label={t("yours")}
/>
<Divider />
{teams?.map((team) => (
<FilterCheckboxField
key={team.id}
id={team.name}
label={team.name}
checked={!!query.teamIds?.includes(team.id)}
onChange={(e) => {
if (e.target.checked) {
pushItemToKey("teamIds", team.id);
} else if (!e.target.checked) {
removeItemByKeyAndValue("teamIds", team.id);
{teams
?.filter((team) => !teamMetadataSchema.parse(team.metadata)?.isOrganization)
.map((team) => (
<FilterCheckboxField
key={team.id}
id={team.name}
label={team.name}
checked={!!query.teamIds?.includes(team.id)}
onChange={(e) => {
if (e.target.checked) {
pushItemToKey("teamIds", team.id);
} else if (!e.target.checked) {
removeItemByKeyAndValue("teamIds", team.id);
}
}}
icon={
<Avatar
alt={team?.name}
imageSrc={getPlaceholderAvatar(team.logo, team?.name as string)}
size="xs"
/>
}
}}
icon={
<Avatar
alt={team?.name}
imageSrc={getPlaceholderAvatar(team.logo, team?.name as string)}
size="xs"
/>
}
/>
))}
/>
))}
</FilterCheckboxFieldsContainer>
</AnimatedPopover>
{showVerticalDivider && <VerticalDivider />}
@@ -1,4 +1,3 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { Suspense } from "react";
@@ -6,20 +5,9 @@ import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { WebhooksByViewer } from "@calcom/trpc/server/routers/viewer/webhook/getByViewer.handler";
import {
Button,
Meta,
SkeletonText,
EmptyScreen,
Dropdown,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownItem,
DropdownMenuLabel,
} from "@calcom/ui";
import { Meta, EmptyScreen, CreateButtonWithTeamsList } from "@calcom/ui";
import { Avatar } from "@calcom/ui";
import { Plus, Link as LinkIcon } from "@calcom/ui/components/icon";
import { Link as LinkIcon } from "@calcom/ui/components/icon";
import { getLayout } from "../../settings/layouts/SettingsLayout";
import { WebhookListItem, WebhookListSkeleton } from "../components";
@@ -33,14 +21,24 @@ const WebhooksView = () => {
enabled: router.isReady,
});
const profiles = data?.profiles.filter((profile) => !profile.readOnly);
return (
<>
<Meta
title="Webhooks"
description={t("add_webhook_description", { appName: APP_NAME })}
CTA={data && data.webhookGroups.length > 0 ? <NewWebhookButton profiles={profiles} /> : <></>}
CTA={
data && data.webhookGroups.length > 0 ? (
<CreateButtonWithTeamsList
subtitle={t("create_for").toUpperCase()}
createFunction={(teamId?: number) => {
router.push(`webhooks/new${teamId ? `?teamId=${teamId}` : ""}`);
}}
data-testid="new_webhook"
/>
) : (
<></>
)
}
/>
<div>
<Suspense fallback={<WebhookListSkeleton />}>
@@ -51,68 +49,6 @@ const WebhooksView = () => {
);
};
const NewWebhookButton = ({
teamId,
profiles,
}: {
teamId?: number | null;
profiles?: {
readOnly?: boolean | undefined;
slug: string | null;
name: string | null;
image?: string | undefined;
teamId: number | null | undefined;
}[];
}) => {
const { t, isLocaleReady } = useLocale();
const url = new URL(`${WEBAPP_URL}/settings/developer/webhooks/new`);
if (!!teamId) {
url.searchParams.set("teamId", `${teamId}`);
}
const href = url.href;
if (!profiles || profiles.length < 2) {
return (
<Button color="primary" data-testid="new_webhook" StartIcon={Plus} href={href}>
{isLocaleReady ? t("new") : <SkeletonText className="h-4 w-24" />}
</Button>
);
}
return (
<Dropdown>
<DropdownMenuTrigger asChild>
<Button color="primary" StartIcon={Plus}>
{isLocaleReady ? t("new") : <SkeletonText className="h-4 w-24" />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={14} align="end">
<DropdownMenuLabel>
<div className="text-xs">{t("create_for").toUpperCase()}</div>
</DropdownMenuLabel>
{profiles.map((profile, idx) => (
<DropdownMenuItem key={profile.slug}>
<DropdownItem
type="button"
StartIcon={(props) => (
<Avatar
alt={profile.slug || ""}
imageSrc={profile.image || `${WEBAPP_URL}/${profile.name}/avatar.png`}
size="sm"
{...props}
/>
)}>
<Link href={`webhooks/new${profile.teamId ? `?teamId=${profile.teamId}` : ""}`}>
{profile.name}
</Link>
</DropdownItem>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</Dropdown>
);
};
const WebhooksList = ({ webhooksByViewer }: { webhooksByViewer: WebhooksByViewer }) => {
const { t } = useLocale();
const router = useRouter();
@@ -166,7 +102,15 @@ const WebhooksList = ({ webhooksByViewer }: { webhooksByViewer: WebhooksByViewer
Icon={LinkIcon}
headline={t("create_your_first_webhook")}
description={t("create_your_first_webhook_description", { appName: APP_NAME })}
buttonRaw={<NewWebhookButton profiles={profiles} />}
buttonRaw={
<CreateButtonWithTeamsList
subtitle={t("create_for").toUpperCase()}
createFunction={(teamId?: number) => {
router.push(`webhooks/new${teamId ? `?teamId=${teamId}` : ""}`);
}}
data-testid="new_webhook"
/>
}
/>
)}
</>
@@ -68,6 +68,12 @@ const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
...baseEventTypeSelect,
});
export const compareMembership = (mship1: MembershipRole, mship2: MembershipRole) => {
const mshipToNumber = (mship: MembershipRole) =>
Object.keys(MembershipRole).findIndex((mmship) => mmship === mship);
return mshipToNumber(mship1) > mshipToNumber(mship2);
};
export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) => {
const { prisma } = ctx;
@@ -214,12 +220,6 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
membershipRole: membership.role,
}));
const compareMembership = (mship1: MembershipRole, mship2: MembershipRole) => {
const mshipToNumber = (mship: MembershipRole) =>
Object.keys(MembershipRole).findIndex((mmship) => mmship === mship);
return mshipToNumber(mship1) > mshipToNumber(mship2);
};
const filterTeamsEventTypesBasedOnInput = (eventType: ReturnType<typeof mapEventType>) => {
if (!input?.filters || !hasFilter(input?.filters)) {
return true;
@@ -2,10 +2,13 @@ import { CAL_URL } from "@calcom/lib/constants";
import { prisma } from "@calcom/prisma";
import type { Webhook } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import { compareMembership } from "../eventTypes/getByViewer.handler";
type GetByViewerOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
@@ -57,6 +60,8 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
id: true,
name: true,
slug: true,
parentId: true,
metadata: true,
members: {
select: {
userId: true,
@@ -91,19 +96,44 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
},
});
const teamWebhookGroups: WebhookGroup[] = user.teams.map((membership) => ({
const teamMemberships = user.teams.map((membership) => ({
teamId: membership.team.id,
profile: {
name: membership.team.name,
slug: "team/" + membership.team.slug,
image: `${CAL_URL}/team/${membership.team.slug}/avatar.png`,
},
metadata: {
readOnly: membership.role !== MembershipRole.ADMIN && membership.role !== MembershipRole.OWNER,
},
webhooks: membership.team.webhooks,
membershipRole: membership.role,
}));
const teamWebhookGroups: WebhookGroup[] = user.teams
.filter((mmship) => {
const metadata = teamMetadataSchema.parse(mmship.team.metadata);
return !metadata?.isOrganization;
})
.map((membership) => {
const orgMembership = teamMemberships.find(
(teamM) => teamM.teamId === membership.team.parentId
)?.membershipRole;
return {
teamId: membership.team.id,
profile: {
name: membership.team.name,
slug: membership.team.slug
? !membership.team.parentId
? `/team`
: "" + membership.team.slug
: null,
image: `${CAL_URL}/team/${membership.team.slug}/avatar.png`,
},
metadata: {
readOnly:
membership.role ===
(membership.team.parentId
? orgMembership && compareMembership(orgMembership, membership.role)
? orgMembership
: MembershipRole.MEMBER
: MembershipRole.MEMBER),
},
webhooks: membership.team.webhooks,
};
});
webhookGroups = webhookGroups.concat(teamWebhookGroups);
return {
@@ -112,6 +142,7 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
teamId: group.teamId,
...group.profile,
...group.metadata,
image: `${CAL_URL}/${group.profile.slug}/avatar.png`,
})),
};
};
@@ -3,6 +3,7 @@ import { router } from "../../../trpc";
import { ZActivateEventTypeInputSchema } from "./activateEventType.schema";
import { ZCreateInputSchema } from "./create.schema";
import { ZDeleteInputSchema } from "./delete.schema";
import { ZFilteredListInputSchema } from "./filteredList.schema";
import { ZGetInputSchema } from "./get.schema";
import { ZGetVerifiedNumbersInputSchema } from "./getVerifiedNumbers.schema";
import { ZListInputSchema } from "./list.schema";
@@ -21,7 +22,7 @@ type WorkflowsRouterHandlerCache = {
verifyPhoneNumber?: typeof import("./verifyPhoneNumber.handler").verifyPhoneNumberHandler;
getVerifiedNumbers?: typeof import("./getVerifiedNumbers.handler").getVerifiedNumbersHandler;
getWorkflowActionOptions?: typeof import("./getWorkflowActionOptions.handler").getWorkflowActionOptionsHandler;
getByViewer?: typeof import("./getByViewer.handler").getByViewerHandler;
filteredList?: typeof import("./filteredList.handler").filteredListHandler;
};
const UNSTABLE_HANDLER_CACHE: WorkflowsRouterHandlerCache = {};
@@ -197,21 +198,21 @@ export const workflowsRouter = router({
ctx,
});
}),
getByViewer: authedProcedure.query(async ({ ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.getByViewer) {
UNSTABLE_HANDLER_CACHE.getByViewer = await import("./getByViewer.handler").then(
(mod) => mod.getByViewerHandler
filteredList: authedProcedure.input(ZFilteredListInputSchema).query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.filteredList) {
UNSTABLE_HANDLER_CACHE.filteredList = await import("./filteredList.handler").then(
(mod) => mod.filteredListHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.getByViewer) {
if (!UNSTABLE_HANDLER_CACHE.filteredList) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.getByViewer({
return UNSTABLE_HANDLER_CACHE.filteredList({
ctx,
input,
});
}),
});
@@ -0,0 +1,143 @@
import type { WorkflowType } from "@calcom/ee/workflows/components/WorkflowListPage";
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
import type { Prisma, PrismaClient } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/client";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TFilteredListInputSchema } from "./filteredList.schema";
type FilteredListOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
prisma: PrismaClient;
};
input: TFilteredListInputSchema;
};
const includedFields = {
activeOn: {
select: {
eventType: {
select: {
id: true,
title: true,
parentId: true,
_count: {
select: {
children: true,
},
},
},
},
},
},
steps: true,
team: {
select: {
id: true,
slug: true,
name: true,
members: true,
logo: true,
},
},
};
export const filteredListHandler = async ({ ctx, input }: FilteredListOptions) => {
const { prisma, user } = ctx;
const filters = input?.filters;
const filtered = filters && hasFilter(filters);
const allWorkflows = await prisma.workflow.findMany({
where: {
OR: [
{
userId: user.id,
},
{
team: {
members: {
some: {
userId: user.id,
accepted: true,
},
},
},
},
],
},
include: includedFields,
orderBy: {
id: "asc",
},
});
if (!filtered) {
const workflowsWithReadOnly: WorkflowType[] = allWorkflows.map((workflow) => {
const readOnly = !!workflow.team?.members?.find(
(member) => member.userId === ctx.user.id && member.role === MembershipRole.MEMBER
);
return { readOnly, ...workflow };
});
return {
filtered: workflowsWithReadOnly,
totalCount: allWorkflows.length,
};
}
const where = {
OR: [] as Prisma.WorkflowWhereInput[],
};
if (filtered) {
if (!!filters.teamIds) {
where.OR.push({
team: {
id: {
in: filters.teamIds ?? [],
},
members: {
some: {
userId: user.id,
accepted: true,
},
},
},
});
}
if (!!filters.userIds) {
where.OR.push({
userId: {
in: filters.userIds,
},
teamId: null,
});
}
const filteredWorkflows = await prisma.workflow.findMany({
where,
include: includedFields,
orderBy: {
id: "asc",
},
});
const workflowsWithReadOnly: WorkflowType[] = filteredWorkflows.map((workflow) => {
const readOnly = !!workflow.team?.members?.find(
(member) => member.userId === ctx.user.id && member.role === MembershipRole.MEMBER
);
return { readOnly, ...workflow };
});
return {
filtered: workflowsWithReadOnly,
totalCount: allWorkflows.length,
};
}
};
@@ -0,0 +1,11 @@
import { z } from "zod";
import { filterQuerySchemaStrict } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
export const ZFilteredListInputSchema = z
.object({
filters: filterQuerySchemaStrict.optional(),
})
.nullish();
export type TFilteredListInputSchema = z.infer<typeof ZFilteredListInputSchema>;
@@ -28,6 +28,7 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
id: true,
slug: true,
members: true,
name: true,
},
},
time: true,
@@ -1,121 +0,0 @@
import { CAL_URL } from "@calcom/lib/constants";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
type GetByViewerOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
const user = await prisma.user.findUnique({
where: {
id: ctx.user.id,
},
select: {
id: true,
username: true,
avatar: 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"];
image?: string;
};
metadata?: {
readOnly: boolean;
};
workflows: typeof userWorkflows;
};
let workflowGroups: WorkflowGroup[] = [];
const image = user?.username ? `${CAL_URL}/${user.username}/avatar.png` : undefined;
workflowGroups.push({
teamId: null,
profile: {
slug: user.username,
name: user.name,
image,
},
workflows: userWorkflows,
metadata: {
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,
image: `${CAL_URL}/team/${membership.team.slug}/avatar.png`,
},
metadata: {
readOnly: membership.role === MembershipRole.MEMBER,
},
workflows: membership.team.workflows,
}))
);
return {
workflowGroups: workflowGroups.filter((groupBy) => !!groupBy.workflows?.length),
profiles: workflowGroups.map((group) => ({
teamId: group.teamId,
...group.profile,
...group.metadata,
})),
};
};
@@ -1 +0,0 @@
export {};
@@ -698,6 +698,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
id: true,
slug: true,
members: true,
name: true,
},
},
steps: {
+2 -1
View File
@@ -12,7 +12,7 @@ import { Tooltip } from "../tooltip";
export type AvatarProps = {
className?: string;
size: "xs" | "xsm" | "sm" | "md" | "mdLg" | "lg" | "xl";
size: "xxs" | "xs" | "xsm" | "sm" | "md" | "mdLg" | "lg" | "xl";
imageSrc?: Maybe<string>;
title?: string;
alt: string;
@@ -24,6 +24,7 @@ export type AvatarProps = {
};
const sizesPropsBySize = {
xxs: "w-3.5 h-3.5 min-w-3.5 min-h-3.5", // 14px
xs: "w-4 h-4 min-w-4 min-h-4 max-h-4", // 16px
xsm: "w-5 h-5 min-w-5 min-h-5", // 20px
sm: "w-6 h-6 min-w-6 min-h-6", // 24px
@@ -3,7 +3,9 @@ import { trpc } from "@calcom/trpc/react";
import type { CreateBtnProps } from "./CreateButton";
import { CreateButton } from "./CreateButton";
export function CreateButtonWithTeamsList(props: Omit<CreateBtnProps, "options">) {
export function CreateButtonWithTeamsList(
props: Omit<CreateBtnProps, "options"> & { onlyShowWithTeams?: boolean; onlyShowWithNoTeams?: boolean }
) {
const query = trpc.viewer.teamsAndUserProfilesQuery.useQuery();
if (!query.data) return null;
@@ -18,5 +20,9 @@ export function CreateButtonWithTeamsList(props: Omit<CreateBtnProps, "options">
};
});
if (props.onlyShowWithTeams && teamsAndUserProfiles.length < 2) return null;
if (props.onlyShowWithNoTeams && teamsAndUserProfiles.length > 1) return null;
return <CreateButton {...props} options={teamsAndUserProfiles} />;
}