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:
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
@@ -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} />
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user