diff --git a/packages/features/ee/teams/components/MemberListItem.tsx b/packages/features/ee/teams/components/MemberListItem.tsx index d0d1466cfe..178650f6b5 100644 --- a/packages/features/ee/teams/components/MemberListItem.tsx +++ b/packages/features/ee/teams/components/MemberListItem.tsx @@ -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) { diff --git a/packages/features/ee/teams/pages/team-profile-view.tsx b/packages/features/ee/teams/pages/team-profile-view.tsx index 9923a268f0..c88bb30aed 100644 --- a/packages/features/ee/teams/pages/team-profile-view.tsx +++ b/packages/features/ee/teams/pages/team-profile-view.tsx @@ -328,40 +328,40 @@ const ProfileView = () => { )} - ):( + ) : ( <>
-
+

- +
- +
-
+
- +
- +
- - + + )} ); diff --git a/packages/features/ee/workflows/components/DeleteDialog.tsx b/packages/features/ee/workflows/components/DeleteDialog.tsx index ddb1848799..433dc30c24 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/EmptyScreen.tsx b/packages/features/ee/workflows/components/EmptyScreen.tsx index d3a7bfaff1..d8a62e0b92 100644 --- a/packages/features/ee/workflows/components/EmptyScreen.tsx +++ b/packages/features/ee/workflows/components/EmptyScreen.tsx @@ -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")}

- 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 e873cbf997..83badc1fea 100644 --- a/packages/features/ee/workflows/components/WorkflowListPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowListPage.tsx @@ -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 )} - {workflow.teamId && ( -
  • - - <>{workflow.team?.name} - -
  • - )} +
    + {workflow.team?.name && ( +
  • + + <>{workflow.team.name} + +
  • + )} +
    +
    +
    + {workflow.team?.name && ( + + +
    {workflow.team.name}
    +
    + )} +
    +
    @@ -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(); }} />
    ) : ( - + <> )} ); diff --git a/packages/features/ee/workflows/pages/index.tsx b/packages/features/ee/workflows/pages/index.tsx index 2331e1d52a..f309d8d5fc 100644 --- a/packages/features/ee/workflows/pages/index.tsx +++ b/packages/features/ee/workflows/pages/index.tsx @@ -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([]); + 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 ( { createMutation.mutate({ teamId }); }} isLoading={createMutation.isLoading} disableMobileButton={true} + onlyShowWithNoTeams={true} /> ) : null }> - {isLoading ? ( - - ) : ( - <> - {query?.data?.profiles && - query?.data?.profiles.length > 1 && - allWorkflowsData?.workflows && - allWorkflowsData.workflows.length && - profileOptions ? ( -
    - + {queryRes.data?.totalCount ? ( +
    + +
    + createMutation.mutate({ teamId })} + isLoading={createMutation.isLoading} + disableMobileButton={true} + onlyShowWithTeams={true} /> -
    - createMutation.mutate({ teamId })} - isLoading={createMutation.isLoading} - disableMobileButton={true} - /> -
    - ) : null} - {profileOptions && profileOptions?.length ? ( - - ) : null} - - )} +
    + ) : null} + } + noResultsScreen={} + SkeletonLoader={SkeletonLoader}> + + + ); diff --git a/packages/features/ee/workflows/pages/workflow.tsx b/packages/features/ee/workflows/pages/workflow.tsx index fb5868e431..3c066c733e 100644 --- a/packages/features/ee/workflows/pages/workflow.tsx +++ b/packages/features/ee/workflows/pages/workflow.tsx @@ -271,7 +271,7 @@ function WorkflowPage() {
    {workflow && workflow.team && ( - {workflow.team.slug} + {workflow.team.name} )} {readOnly && ( diff --git a/packages/features/filters/components/TeamsFilter.tsx b/packages/features/filters/components/TeamsFilter.tsx index 6e070c50fd..81b473021e 100644 --- a/packages/features/filters/components/TeamsFilter.tsx +++ b/packages/features/filters/components/TeamsFilter.tsx @@ -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")} /> - {teams?.map((team) => ( - { - 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) => ( + { + if (e.target.checked) { + pushItemToKey("teamIds", team.id); + } else if (!e.target.checked) { + removeItemByKeyAndValue("teamIds", team.id); + } + }} + icon={ + } - }} - icon={ - - } - /> - ))} + /> + ))} {showVerticalDivider && } diff --git a/packages/features/webhooks/pages/webhooks-view.tsx b/packages/features/webhooks/pages/webhooks-view.tsx index 7cca44f188..a74125bfe4 100644 --- a/packages/features/webhooks/pages/webhooks-view.tsx +++ b/packages/features/webhooks/pages/webhooks-view.tsx @@ -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 ( <> 0 ? : <>} + CTA={ + data && data.webhookGroups.length > 0 ? ( + { + router.push(`webhooks/new${teamId ? `?teamId=${teamId}` : ""}`); + }} + data-testid="new_webhook" + /> + ) : ( + <> + ) + } />
    }> @@ -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 ( - - ); - } - return ( - - - - - - -
    {t("create_for").toUpperCase()}
    -
    - {profiles.map((profile, idx) => ( - - ( - - )}> - - {profile.name} - - - - ))} -
    -
    - ); -}; - 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={} + buttonRaw={ + { + router.push(`webhooks/new${teamId ? `?teamId=${teamId}` : ""}`); + }} + data-testid="new_webhook" + /> + } /> )} diff --git a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts index d7e36a9598..ac04f17b3e 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts @@ -68,6 +68,12 @@ const eventTypeSelect = Prisma.validator()({ ...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) => { if (!input?.filters || !hasFilter(input?.filters)) { return true; diff --git a/packages/trpc/server/routers/viewer/webhook/getByViewer.handler.ts b/packages/trpc/server/routers/viewer/webhook/getByViewer.handler.ts index 6faaea5053..b0b33e7fe4 100644 --- a/packages/trpc/server/routers/viewer/webhook/getByViewer.handler.ts +++ b/packages/trpc/server/routers/viewer/webhook/getByViewer.handler.ts @@ -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; @@ -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`, })), }; }; diff --git a/packages/trpc/server/routers/viewer/workflows/_router.tsx b/packages/trpc/server/routers/viewer/workflows/_router.tsx index 8b8033b37b..1d146aa05c 100644 --- a/packages/trpc/server/routers/viewer/workflows/_router.tsx +++ b/packages/trpc/server/routers/viewer/workflows/_router.tsx @@ -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, }); }), }); diff --git a/packages/trpc/server/routers/viewer/workflows/filteredList.handler.tsx b/packages/trpc/server/routers/viewer/workflows/filteredList.handler.tsx new file mode 100644 index 0000000000..db2e5b2ec6 --- /dev/null +++ b/packages/trpc/server/routers/viewer/workflows/filteredList.handler.tsx @@ -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; + 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, + }; + } +}; diff --git a/packages/trpc/server/routers/viewer/workflows/filteredList.schema.tsx b/packages/trpc/server/routers/viewer/workflows/filteredList.schema.tsx new file mode 100644 index 0000000000..3bb74fbb1f --- /dev/null +++ b/packages/trpc/server/routers/viewer/workflows/filteredList.schema.tsx @@ -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; diff --git a/packages/trpc/server/routers/viewer/workflows/get.handler.ts b/packages/trpc/server/routers/viewer/workflows/get.handler.ts index e9bdefaa7e..3f74f982b3 100644 --- a/packages/trpc/server/routers/viewer/workflows/get.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/get.handler.ts @@ -28,6 +28,7 @@ export const getHandler = async ({ ctx, input }: GetOptions) => { id: true, slug: true, members: true, + name: true, }, }, time: true, diff --git a/packages/trpc/server/routers/viewer/workflows/getByViewer.handler.ts b/packages/trpc/server/routers/viewer/workflows/getByViewer.handler.ts deleted file mode 100644 index 45050d8df1..0000000000 --- a/packages/trpc/server/routers/viewer/workflows/getByViewer.handler.ts +++ /dev/null @@ -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; - }; -}; - -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, - })), - }; -}; diff --git a/packages/trpc/server/routers/viewer/workflows/getByViewer.schema.ts b/packages/trpc/server/routers/viewer/workflows/getByViewer.schema.ts deleted file mode 100644 index cb0ff5c3b5..0000000000 --- a/packages/trpc/server/routers/viewer/workflows/getByViewer.schema.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/trpc/server/routers/viewer/workflows/update.handler.ts b/packages/trpc/server/routers/viewer/workflows/update.handler.ts index 64c7fe0ed9..8c5f4f6464 100644 --- a/packages/trpc/server/routers/viewer/workflows/update.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/update.handler.ts @@ -698,6 +698,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { id: true, slug: true, members: true, + name: true, }, }, steps: { diff --git a/packages/ui/components/avatar/Avatar.tsx b/packages/ui/components/avatar/Avatar.tsx index de59434da1..1c6c4c34c5 100644 --- a/packages/ui/components/avatar/Avatar.tsx +++ b/packages/ui/components/avatar/Avatar.tsx @@ -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; 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 diff --git a/packages/ui/components/createButton/CreateButtonWithTeamsList.tsx b/packages/ui/components/createButton/CreateButtonWithTeamsList.tsx index 5aabdfb5e7..f5b8f86f89 100644 --- a/packages/ui/components/createButton/CreateButtonWithTeamsList.tsx +++ b/packages/ui/components/createButton/CreateButtonWithTeamsList.tsx @@ -3,7 +3,9 @@ import { trpc } from "@calcom/trpc/react"; import type { CreateBtnProps } from "./CreateButton"; import { CreateButton } from "./CreateButton"; -export function CreateButtonWithTeamsList(props: Omit) { +export function CreateButtonWithTeamsList( + props: Omit & { onlyShowWithTeams?: boolean; onlyShowWithNoTeams?: boolean } +) { const query = trpc.viewer.teamsAndUserProfilesQuery.useQuery(); if (!query.data) return null; @@ -18,5 +20,9 @@ export function CreateButtonWithTeamsList(props: Omit }; }); + if (props.onlyShowWithTeams && teamsAndUserProfiles.length < 2) return null; + + if (props.onlyShowWithNoTeams && teamsAndUserProfiles.length > 1) return null; + return ; }