From 871e17a8656091a106bda4b10e4085d5b5bfe2cb Mon Sep 17 00:00:00 2001 From: alannnc Date: Wed, 2 Aug 2023 12:09:43 -0700 Subject: [PATCH] feat: org owner-admin cant see the teams created by other owner-admin 9982 cal 2120 (#10430) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP * New files for other teams * Update code domain model location * Fixes for finding teams in orgs * VerticalTabs remove other teams appearance and only display if org admin * Added conditional to consider orgs admin when updating team profile * team-member-list wip * Update memberInviteModal with disableCopyLink prop * cleaning up * Undo changes to files * remove logs * clean up * undo changes * undo change * Undo changes * Reset files to main * typo on import * Update packages/features/ee/organizations/pages/components/OtherTeamListItem.tsx Co-authored-by: Omar López * useSearchParams hook instead of useRouter * remove unused code and clean up * Fix type --------- Co-authored-by: Omar López --- .../teams/other/[id]/appearance.tsx | 9 + .../teams/other/[id]/members.tsx | 9 + .../teams/other/[id]/profile.tsx | 9 + .../organizations/teams/other/index.ts | 9 + apps/web/public/static/locales/en/common.json | 4 + .../pages/components/MemberListItem.tsx | 112 +++++ .../pages/components/OtherTeamList.tsx | 58 +++ .../pages/components/OtherTeamListItem.tsx | 166 +++++++ .../pages/components/OtherTeamsListing.tsx | 42 ++ .../settings/other-team-listing-view.tsx | 19 + .../settings/other-team-members-view.tsx | 214 ++++++++ .../settings/other-team-profile-view.tsx | 359 ++++++++++++++ .../components/MemberInvitationModal.tsx | 44 +- .../ee/teams/components/MemberListItem.tsx | 6 +- .../ee/teams/pages/team-listing-view.tsx | 6 +- .../settings/layouts/SettingsLayout.tsx | 464 ++++++++++++------ .../server/middlewares/sessionMiddleware.ts | 9 + .../trpc/server/procedures/authedProcedure.ts | 3 +- .../routers/viewer/organizations/_router.tsx | 62 ++- .../organizations/getOtherTeam.handler.ts | 54 ++ .../listOtherTeamMembers.handler.ts | 81 +++ .../organizations/listOtherTeams.handler.ts | 43 ++ .../routers/viewer/teams/update.handler.ts | 8 +- 23 files changed, 1602 insertions(+), 188 deletions(-) create mode 100644 apps/web/pages/settings/organizations/teams/other/[id]/appearance.tsx create mode 100644 apps/web/pages/settings/organizations/teams/other/[id]/members.tsx create mode 100644 apps/web/pages/settings/organizations/teams/other/[id]/profile.tsx create mode 100644 apps/web/pages/settings/organizations/teams/other/index.ts create mode 100644 packages/features/ee/organizations/pages/components/MemberListItem.tsx create mode 100644 packages/features/ee/organizations/pages/components/OtherTeamList.tsx create mode 100644 packages/features/ee/organizations/pages/components/OtherTeamListItem.tsx create mode 100644 packages/features/ee/organizations/pages/components/OtherTeamsListing.tsx create mode 100644 packages/features/ee/organizations/pages/settings/other-team-listing-view.tsx create mode 100644 packages/features/ee/organizations/pages/settings/other-team-members-view.tsx create mode 100644 packages/features/ee/organizations/pages/settings/other-team-profile-view.tsx create mode 100644 packages/trpc/server/routers/viewer/organizations/getOtherTeam.handler.ts create mode 100644 packages/trpc/server/routers/viewer/organizations/listOtherTeamMembers.handler.ts create mode 100644 packages/trpc/server/routers/viewer/organizations/listOtherTeams.handler.ts diff --git a/apps/web/pages/settings/organizations/teams/other/[id]/appearance.tsx b/apps/web/pages/settings/organizations/teams/other/[id]/appearance.tsx new file mode 100644 index 0000000000..6c6343e791 --- /dev/null +++ b/apps/web/pages/settings/organizations/teams/other/[id]/appearance.tsx @@ -0,0 +1,9 @@ +import TeamAppearenceView from "@calcom/features/ee/teams/pages/team-appearance-view"; + +import type { CalPageWrapper } from "@components/PageWrapper"; +import PageWrapper from "@components/PageWrapper"; + +const Page = TeamAppearenceView as CalPageWrapper; +Page.PageWrapper = PageWrapper; + +export default Page; diff --git a/apps/web/pages/settings/organizations/teams/other/[id]/members.tsx b/apps/web/pages/settings/organizations/teams/other/[id]/members.tsx new file mode 100644 index 0000000000..14c0026a46 --- /dev/null +++ b/apps/web/pages/settings/organizations/teams/other/[id]/members.tsx @@ -0,0 +1,9 @@ +import TeamMembersView from "@calcom/features/ee/organizations/pages/settings/other-team-members-view"; + +import type { CalPageWrapper } from "@components/PageWrapper"; +import PageWrapper from "@components/PageWrapper"; + +const Page = TeamMembersView as CalPageWrapper; +Page.PageWrapper = PageWrapper; + +export default Page; diff --git a/apps/web/pages/settings/organizations/teams/other/[id]/profile.tsx b/apps/web/pages/settings/organizations/teams/other/[id]/profile.tsx new file mode 100644 index 0000000000..ad4cba98e5 --- /dev/null +++ b/apps/web/pages/settings/organizations/teams/other/[id]/profile.tsx @@ -0,0 +1,9 @@ +import OtherTeamProfileView from "@calcom/features/ee/organizations/pages/settings/other-team-profile-view"; + +import type { CalPageWrapper } from "@components/PageWrapper"; +import PageWrapper from "@components/PageWrapper"; + +const Page = OtherTeamProfileView as CalPageWrapper; +Page.PageWrapper = PageWrapper; + +export default Page; diff --git a/apps/web/pages/settings/organizations/teams/other/index.ts b/apps/web/pages/settings/organizations/teams/other/index.ts new file mode 100644 index 0000000000..06052c1f3e --- /dev/null +++ b/apps/web/pages/settings/organizations/teams/other/index.ts @@ -0,0 +1,9 @@ +import OtherTeamListView from "@calcom/features/ee/organizations/pages/settings/other-team-listing-view"; + +import PageWrapper from "@components/PageWrapper"; +import type { CalPageWrapper } from "@components/PageWrapper"; + +const Page = OtherTeamListView as CalPageWrapper; +Page.PageWrapper = PageWrapper; + +export default Page; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index b76aaacd22..4d5402ac18 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1972,5 +1972,9 @@ "what_is_this_meeting_about": "What is this meeting about?", "requires_booker_email_verification": "Requires booker email verification", "description_requires_booker_email_verification": "To ensure booker's email verification before scheduling events", + "org_admin_other_teams": "Other teams", + "org_admin_other_teams_description": "Here you can see teams inside your organization but that you are not part of. You can add yourself to them if needed.", + "no_other_teams_found": "No other teams found", + "no_other_teams_found_description": "There are no other teams in this organization.", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/packages/features/ee/organizations/pages/components/MemberListItem.tsx b/packages/features/ee/organizations/pages/components/MemberListItem.tsx new file mode 100644 index 0000000000..fd52ecd384 --- /dev/null +++ b/packages/features/ee/organizations/pages/components/MemberListItem.tsx @@ -0,0 +1,112 @@ +import classNames from "classnames"; + +import TeamPill, { TeamRole } from "@calcom/ee/teams/components/TeamPill"; +import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { RouterOutputs } from "@calcom/trpc/react"; +import { + Avatar, + Button, + ButtonGroup, + Dropdown, + DropdownItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Tooltip, +} from "@calcom/ui"; +import { ExternalLink, MoreHorizontal } from "@calcom/ui/components/icon"; + +interface Props { + member: RouterOutputs["viewer"]["organizations"]["listOtherTeamMembers"][number]; +} + +export default function MemberListItem(props: Props) { + const { t } = useLocale(); + const { member } = props; + + const { user } = member; + const bookerUrl = useBookerUrl(); + const bookerUrlWithoutProtocol = bookerUrl.replace(/^https?:\/\//, ""); + const bookingLink = user.username && `${bookerUrlWithoutProtocol}/${user.username}`; + const name = user.name || user.username || user.email; + + return ( +
  • +
    +
    +
    + + +
    +
    + {name} + + {!props.member.accepted && } + {props.member.role && } +
    +
    + + {user.email} + + {user.username != null && ( + <> + + + {bookingLink} + + + )} +
    +
    +
    +
    + {member.accepted && user.username && ( +
    +
    +
    + )} + +
  • + ); +} diff --git a/packages/features/ee/organizations/pages/components/OtherTeamList.tsx b/packages/features/ee/organizations/pages/components/OtherTeamList.tsx new file mode 100644 index 0000000000..014106d078 --- /dev/null +++ b/packages/features/ee/organizations/pages/components/OtherTeamList.tsx @@ -0,0 +1,58 @@ +import { useState } from "react"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { RouterOutputs } from "@calcom/trpc/react"; +import { trpc } from "@calcom/trpc/react"; +import { showToast } from "@calcom/ui"; + +import OtherTeamListItem from "./OtherTeamListItem"; + +interface Props { + teams: RouterOutputs["viewer"]["organizations"]["listOtherTeams"]; + pending?: boolean; +} + +export default function OtherTeamList(props: Props) { + const utils = trpc.useContext(); + + const { t } = useLocale(); + + const [hideDropdown, setHideDropdown] = useState(false); + + function selectAction(action: string, teamId: number) { + switch (action) { + case "disband": + deleteTeam(teamId); + break; + } + } + + const deleteTeamMutation = trpc.viewer.teams.delete.useMutation({ + async onSuccess() { + await utils.viewer.teams.list.invalidate(); + await utils.viewer.teams.hasTeamPlan.invalidate(); + }, + async onError(err) { + showToast(err.message, "error"); + }, + }); + + function deleteTeam(teamId: number) { + deleteTeamMutation.mutate({ teamId }); + } + + return ( +
      + {props.teams.map((team) => ( + selectAction(action, team?.id as number)} + isLoading={deleteTeamMutation.isLoading} + hideDropdown={hideDropdown} + setHideDropdown={setHideDropdown} + /> + ))} +
    + ); +} diff --git a/packages/features/ee/organizations/pages/components/OtherTeamListItem.tsx b/packages/features/ee/organizations/pages/components/OtherTeamListItem.tsx new file mode 100644 index 0000000000..d160b385de --- /dev/null +++ b/packages/features/ee/organizations/pages/components/OtherTeamListItem.tsx @@ -0,0 +1,166 @@ +import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { MembershipRole } from "@calcom/prisma/enums"; +import type { RouterOutputs } from "@calcom/trpc/react"; +import { + Avatar, + Button, + ButtonGroup, + ConfirmationDialogContent, + Dialog, + DialogTrigger, + Dropdown, + DropdownItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, + showToast, + Tooltip, +} from "@calcom/ui"; +import { Edit2, ExternalLink, Link as LinkIcon, MoreHorizontal, Trash } from "@calcom/ui/components/icon"; + +import { useOrgBranding } from "../../../organizations/context/provider"; + +interface Props { + team: RouterOutputs["viewer"]["organizations"]["listOtherTeams"][number]; + key: number; + onActionSelect: (text: string) => void; + isLoading?: boolean; + hideDropdown: boolean; + setHideDropdown: (value: boolean) => void; +} + +export default function OtherTeamListItem(props: Props) { + const { t } = useLocale(); + + const team = props.team; + + const orgBranding = useOrgBranding(); + + const isOwner = props.team.role === MembershipRole.OWNER; + + const { hideDropdown, setHideDropdown } = props; + + if (!team) return <>; + + const teamInfo = ( +
    + +
    + {team.name} + + {team.slug + ? orgBranding + ? `${orgBranding.fullDomain}${team.slug}` + : `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/${team.slug}` + : "Unpublished team"} + +
    +
    + ); + + return ( +
  • +
    + {teamInfo} +
    +
    + + {team.slug && ( + +
    +
    +
    +
  • + ); +} diff --git a/packages/features/ee/organizations/pages/components/OtherTeamsListing.tsx b/packages/features/ee/organizations/pages/components/OtherTeamsListing.tsx new file mode 100644 index 0000000000..153ec79155 --- /dev/null +++ b/packages/features/ee/organizations/pages/components/OtherTeamsListing.tsx @@ -0,0 +1,42 @@ +import { useMemo, useState } from "react"; + +import SkeletonLoaderTeamList from "@calcom/ee/teams/components/SkeletonloaderTeamList"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; +import { Alert, EmptyScreen } from "@calcom/ui"; + +import OtherTeamList from "./OtherTeamList"; + +export function OtherTeamsListing() { + const { t } = useLocale(); + + const [errorMessage, setErrorMessage] = useState(""); + + const { data, isLoading } = trpc.viewer.organizations.listOtherTeams.useQuery(undefined, { + onError: (e) => { + setErrorMessage(e.message); + }, + }); + + const teams = useMemo(() => data?.filter((m) => m.accepted) || [], [data]); + + if (isLoading) { + return ; + } + + return ( + <> + {!!errorMessage && } + + {teams.length > 0 ? ( + + ) : ( + + )} + + ); +} diff --git a/packages/features/ee/organizations/pages/settings/other-team-listing-view.tsx b/packages/features/ee/organizations/pages/settings/other-team-listing-view.tsx new file mode 100644 index 0000000000..7597a8f116 --- /dev/null +++ b/packages/features/ee/organizations/pages/settings/other-team-listing-view.tsx @@ -0,0 +1,19 @@ +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Meta } from "@calcom/ui"; + +import { getLayout } from "../../../../settings/layouts/SettingsLayout"; +import { OtherTeamsListing } from "./../components/OtherTeamsListing"; + +const OtherTeamListingView = (): React.ReactElement => { + const { t } = useLocale(); + return ( + <> + + + + ); +}; + +OtherTeamListingView.getLayout = getLayout; + +export default OtherTeamListingView; diff --git a/packages/features/ee/organizations/pages/settings/other-team-members-view.tsx b/packages/features/ee/organizations/pages/settings/other-team-members-view.tsx new file mode 100644 index 0000000000..1ac4cec97e --- /dev/null +++ b/packages/features/ee/organizations/pages/settings/other-team-members-view.tsx @@ -0,0 +1,214 @@ +// import { debounce } from "lodash"; +import { useSearchParams, useRouter } from "next/navigation"; +import { useState, useEffect } from "react"; + +import MemberInvitationModal from "@calcom/ee/teams/components/MemberInvitationModal"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; +import type { RouterOutputs } from "@calcom/trpc/react"; +import { Meta, showToast } from "@calcom/ui"; + +import { getLayout } from "../../../../settings/layouts/SettingsLayout"; +import MakeTeamPrivateSwitch from "../../../teams/components/MakeTeamPrivateSwitch"; +import MemberListItem from "../components/MemberListItem"; + +type Members = RouterOutputs["viewer"]["organizations"]["listOtherTeamMembers"]; +type Team = RouterOutputs["viewer"]["organizations"]["getOtherTeam"]; + +interface MembersListProps { + members: Members | undefined; + team: Team | undefined; + offset: number; + setOffset: (offset: number) => void; + displayLoadMore: boolean; +} + +function MembersList(props: MembersListProps) { + const { t } = useLocale(); + const { displayLoadMore, members, team } = props; + return ( +
    + {members?.length && team ? ( +
      + {members.map((member) => { + return ; + })} +
    + ) : null} + {displayLoadMore && ( + + )} +
    + ); +} + +const MembersView = () => { + const { t, i18n } = useLocale(); + const router = useRouter(); + const searchParams = useSearchParams(); + const teamId = Number(searchParams.get("id")); + + const utils = trpc.useContext(); + const [offset, setOffset] = useState(1); + // const [query, setQuery] = useState(""); + // const [queryToFetch, setQueryToFetch] = useState(""); + const [loadMore, setLoadMore] = useState(true); + const limit = 100; + const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false); + const [members, setMembers] = useState([]); + const { data: team, isLoading: isTeamLoading } = trpc.viewer.organizations.getOtherTeam.useQuery( + { teamId }, + { + onError: () => { + router.push("/settings"); + }, + } + ); + const { data: membersFetch, isLoading: isLoadingMembers } = + trpc.viewer.organizations.listOtherTeamMembers.useQuery( + { teamId, limit, offset: (offset - 1) * limit }, + { + onError: () => { + router.push("/settings"); + }, + } + ); + + useEffect(() => { + if (membersFetch) { + if (membersFetch.length < limit) { + setLoadMore(false); + } else { + setLoadMore(true); + } + setMembers(members.concat(membersFetch)); + } + }, [membersFetch]); + + // useEffect(() => { + // if (queryToFetch !== "") { + // setMembers(membersFetch || []); + // setLoadMore(false); + // } + // }, [membersFetch, query]); + + const isLoading = isTeamLoading || isLoadingMembers; + + const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation(); + + // const debouncedFunction = debounce((query) => { + // setQueryToFetch(query); + // }, 500); + + return ( + <> + setShowMemberInvitationModal(true)} + // data-testid="new-member-button"> + // {t("add")} + // + // } + /> + {!isLoading && ( + <> +
    + <> + {/* Currently failing due to re render and loose focus */} + {/* { + setQuery(e.target.value); + debouncedFunction(e.target.value); + }} + value={query} + placeholder={`${t("search")}...`} + /> */} + + + + {team && ( + <> +
    + + + )} +
    + {showMemberInvitationModal && team && ( + setShowMemberInvitationModal(false)} + onSubmit={(values, resetFields) => { + inviteMemberMutation.mutate( + { + teamId, + language: i18n.language, + role: values.role, + usernameOrEmail: values.emailOrUsername, + sendEmailInvitation: values.sendInviteEmail, + }, + { + onSuccess: async (data) => { + await utils.viewer.teams.get.invalidate(); + setShowMemberInvitationModal(false); + if (data.sendEmailInvitation) { + if (Array.isArray(data.usernameOrEmail)) { + showToast( + t("email_invite_team_bulk", { + userCount: data.usernameOrEmail.length, + }), + "success" + ); + resetFields(); + } else { + showToast( + t("email_invite_team", { + email: data.usernameOrEmail, + }), + "success" + ); + } + } + }, + onError: (error) => { + showToast(error.message, "error"); + }, + } + ); + }} + onSettingsOpen={() => { + setShowMemberInvitationModal(false); + }} + /> + )} + + )} + + ); +}; + +MembersView.getLayout = getLayout; + +export default MembersView; diff --git a/packages/features/ee/organizations/pages/settings/other-team-profile-view.tsx b/packages/features/ee/organizations/pages/settings/other-team-profile-view.tsx new file mode 100644 index 0000000000..c9de131e87 --- /dev/null +++ b/packages/features/ee/organizations/pages/settings/other-team-profile-view.tsx @@ -0,0 +1,359 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import type { Prisma } from "@prisma/client"; +import { useSession } from "next-auth/react"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useState, useLayoutEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { z } from "zod"; + +import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; +import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { md } from "@calcom/lib/markdownIt"; +import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; +import objectKeys from "@calcom/lib/objectKeys"; +import turndown from "@calcom/lib/turndownService"; +import type { RouterOutputs } from "@calcom/trpc/react"; +import { trpc } from "@calcom/trpc/react"; +import { SkeletonContainer, SkeletonText } from "@calcom/ui"; +import { + Avatar, + Button, + ConfirmationDialogContent, + Dialog, + DialogTrigger, + Form, + ImageUploader, + Label, + LinkIconButton, + Meta, + showToast, + TextField, + Editor, +} from "@calcom/ui"; +import { ExternalLink, Link as LinkIcon, Trash2 } from "@calcom/ui/components/icon"; + +import { getLayout } from "../../../../settings/layouts/SettingsLayout"; +import { extractDomainFromWebsiteUrl } from "../../../organizations/lib/utils"; + +const regex = new RegExp("^[a-zA-Z0-9-]*$"); + +const teamProfileFormSchema = z.object({ + name: z.string(), + slug: z + .string() + .regex(regex, { + message: "Url can only have alphanumeric characters(a-z, 0-9) and hyphen(-) symbol.", + }) + .min(1, { message: "Url cannot be left empty" }), + logo: z.string(), + bio: z.string(), +}); + +const OtherTeamProfileView = () => { + const { t } = useLocale(); + const router = useRouter(); + const utils = trpc.useContext(); + const session = useSession(); + const [firstRender, setFirstRender] = useState(true); + + useLayoutEffect(() => { + document.body.focus(); + }, []); + + const mutation = trpc.viewer.teams.update.useMutation({ + onError: (err) => { + showToast(err.message, "error"); + }, + async onSuccess() { + await utils.viewer.teams.get.invalidate(); + showToast(t("your_team_updated_successfully"), "success"); + }, + }); + + const form = useForm({ + resolver: zodResolver(teamProfileFormSchema), + }); + const searchParams = useSearchParams(); + const teamId = Number(searchParams.get("id")); + const { data: team, isLoading } = trpc.viewer.organizations.getOtherTeam.useQuery( + { teamId: teamId }, + { + enabled: !!teamId, + onError: () => { + router.push("/settings"); + }, + onSuccess: (team: RouterOutputs["viewer"]["organizations"]["getOtherTeam"]) => { + if (team) { + form.setValue("name", team.name || ""); + form.setValue("slug", team.slug || ""); + form.setValue("logo", team.logo || ""); + form.setValue("bio", team.bio || ""); + if (team.slug === null && (team?.metadata as Prisma.JsonObject)?.requestedSlug) { + form.setValue("slug", ((team?.metadata as Prisma.JsonObject)?.requestedSlug as string) || ""); + } + } + }, + } + ); + + // This page can only be accessed by team admins (owner/admin) + const isAdmin = true; + + const permalink = `${WEBAPP_URL}/team/${team?.slug}`; + + const isBioEmpty = !team || !team.bio || !team.bio.replace("


    ", "").length; + + const deleteTeamMutation = trpc.viewer.teams.delete.useMutation({ + async onSuccess() { + await utils.viewer.teams.list.invalidate(); + showToast(t("your_team_disbanded_successfully"), "success"); + router.push(`${WEBAPP_URL}/teams`); + }, + }); + + const removeMemberMutation = trpc.viewer.teams.removeMember.useMutation({ + async onSuccess() { + await utils.viewer.teams.get.invalidate(); + await utils.viewer.teams.list.invalidate(); + await utils.viewer.eventTypes.invalidate(); + showToast(t("success"), "success"); + }, + async onError(err) { + showToast(err.message, "error"); + }, + }); + + const publishMutation = trpc.viewer.teams.publish.useMutation({ + async onSuccess(data: { url?: string }) { + if (data.url) { + router.push(data.url); + } + }, + async onError(err) { + showToast(err.message, "error"); + }, + }); + + function deleteTeam() { + if (team?.id) deleteTeamMutation.mutate({ teamId: team.id }); + } + + function leaveTeam() { + if (team?.id && session.data) + removeMemberMutation.mutate({ + teamId: team.id, + memberId: session.data.user.id, + }); + } + + if (!team) return null; + + return ( + <> + + {!isLoading ? ( + <> + {isAdmin ? ( +
    { + if (team) { + const variables = { + logo: values.logo, + name: values.name, + slug: values.slug, + bio: values.bio, + }; + objectKeys(variables).forEach((key) => { + if (variables[key as keyof typeof variables] === team?.[key]) delete variables[key]; + }); + mutation.mutate({ id: team.id, ...variables }); + } + }}> +
    + ( + <> + +
    + { + form.setValue("logo", newLogo); + }} + imageSrc={value} + /> +
    + + )} + /> +
    + +
    + + ( +
    + { + form.setValue("name", e?.target.value); + }} + /> +
    + )} + /> + ( +
    + { + form.clearErrors("slug"); + form.setValue("slug", e?.target.value); + }} + /> +
    + )} + /> +
    + + md.render(form.getValues("bio") || "")} + setText={(value: string) => form.setValue("bio", turndown(value))} + excludedToolbarItems={["blockType"]} + disableLists + firstRender={firstRender} + setFirstRender={setFirstRender} + /> +
    +

    {t("team_description")}

    + + {IS_TEAM_BILLING_ENABLED && + team.slug === null && + (team.metadata as Prisma.JsonObject)?.requestedSlug && ( + + )} + + ) : ( +
    +
    +
    + +

    {team?.name}

    +
    + {team && !isBioEmpty && ( + <> + +
    + + )} +
    +
    + + {t("preview")} + + { + navigator.clipboard.writeText(permalink); + showToast("Copied to clipboard", "success"); + }}> + {t("copy_link_team")} + +
    +
    + )} +
    + +
    {t("danger_zone")}
    + + + + + + + {t("disband_team_confirmation_message")} + + + + ) : ( + <> + +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    + +
    + +
    +
    +
    + +
    + + +
    + +
    + +
    +
    +
    + + )} + + ); +}; + +OtherTeamProfileView.getLayout = getLayout; + +export default OtherTeamProfileView; diff --git a/packages/features/ee/teams/components/MemberInvitationModal.tsx b/packages/features/ee/teams/components/MemberInvitationModal.tsx index 811c030902..f6fad884f0 100644 --- a/packages/features/ee/teams/components/MemberInvitationModal.tsx +++ b/packages/features/ee/teams/components/MemberInvitationModal.tsx @@ -37,9 +37,10 @@ type MemberInvitationModalProps = { onSubmit: (values: NewMemberForm, resetFields: () => void) => void; onSettingsOpen?: () => void; teamId: number; - members: PendingMember[]; + members?: PendingMember[]; token?: string; isLoading?: boolean; + disableCopyLink?: boolean; }; type MembershipRoleOption = { @@ -66,6 +67,7 @@ function toggleElementInArray(value: string[] | string | undefined, element: str export default function MemberInvitationModal(props: MemberInvitationModalProps) { const { t } = useLocale(); + const { disableCopyLink = false } = props; const trpcContext = trpc.useContext(); const [modalImportMode, setModalInputMode] = useState( @@ -119,9 +121,10 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps) const newMemberFormMethods = useForm(); const validateUniqueInvite = (value: string) => { + if (!props?.members?.length) return true; return !( - props.members.some((member) => member?.username === value) || - props.members.some((member) => member?.email === value) + props?.members.some((member) => member?.username === value) || + props?.members.some((member) => member?.email === value) ); }; @@ -351,23 +354,24 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps) )}
    -
    - -
    - + {!disableCopyLink && ( +
    + +
    + )}
    - {props.team.membership.accepted && ( + {props.team.membership?.accepted && (