From 1e816eb6d90c9534b53888ff6ca472c7bdd7b784 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Sun, 23 Jul 2023 01:01:51 +0530 Subject: [PATCH 01/10] feat: added delete card and modal --- .../settings/members/DeleteTeam.tsx | 161 ++++++++++++++++++ .../[environmentId]/settings/members/page.tsx | 6 + .../settings/profile/editProfile.tsx | 4 +- .../[environmentId]/team/index.ts | 2 +- 4 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx new file mode 100644 index 0000000000..34a51e4ac0 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx @@ -0,0 +1,161 @@ +"use client"; + +import DeleteDialog from "@/components/shared/DeleteDialog"; +import LoadingSpinner from "@/components/shared/LoadingSpinner"; +import { useEnvironment } from "@/lib/environments/environments"; +import { useMembers } from "@/lib/members"; +import { useProduct } from "@/lib/products/products"; +import { useProfile } from "@/lib/profile"; +import { truncate } from "@/lib/utils"; +import { Button, ErrorComponent, Input } from "@formbricks/ui"; +import { useTeamMutation } from "@/lib/teams/mutateTeams"; +import { useTeam } from "@/lib/teams/teams"; +import { useState, Dispatch, SetStateAction } from "react"; +import toast from "react-hot-toast"; +import { useMemberships } from "@/lib/memberships"; + +export default function DeleteTeam({ environmentId }) { + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + + const { profile } = useProfile(); + const { memberships } = useMemberships(); + const { team } = useMembers(environmentId); + const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId); + const { environment } = useEnvironment(environmentId); + const { team: teamData } = useTeam(environmentId); + + console.log({ profile, team, product, environment, teamData, memberships }); + + const availableTeams = memberships?.length; + const role = team?.members?.filter((member) => member?.userId === profile?.id)[0]?.role; + const isUserAdminOrOwner = role === "admin" || role === "owner"; + const isDeleteDisabled = availableTeams <= 1 || !isUserAdminOrOwner; + + if (isLoadingProduct) { + return ; + } + if (isErrorProduct) { + return ; + } + + // const handleDeleteProduct = async () => { + // if (environment?.availableProducts?.length <= 1) { + // toast.error("Cannot delete product. Your team needs at least 1."); + // setIsDeleteDialogOpen(false); + // return; + // } + // const deleteProductRes = await deleteProduct(environmentId); + + // if (deleteProductRes?.id?.length > 0) { + // toast.success("Product deleted successfully."); + // // router.push("/"); + // } else if (deleteProductRes?.message?.length > 0) { + // toast.error(deleteProductRes.message); + // setIsDeleteDialogOpen(false); + // } else { + // toast.error("Error deleting product. Please try again."); + // } + // }; + + return ( +
+ {!isDeleteDisabled && ( +
+

+ Delete {truncate(teamData?.name, 30)} +  with all its products incl. all surveys, responses, people, actions and attributes.{" "} + This action cannot be undone. +

+ +
+ )} + {isDeleteDisabled && ( +

+ {!isUserAdminOrOwner + ? "Only Admin or Owners can delete teams." + : "This is your only team, it cannot be deleted. Create a new team first."} +

+ )} + +
+ ); +} + +interface DeleteTeamModalProps { + open: boolean; + setOpen: Dispatch>; + teamData: { name: string; id: string; plan: string }; +} + +function DeleteTeamModal({ setOpen, open, teamData }: DeleteTeamModalProps) { + const [deleting, setDeleting] = useState(false); + const [inputValue, setInputValue] = useState(""); + + const handleInputChange = (e) => { + setInputValue(e.target.value); + }; + + const deleteTeam = async () => { + try { + setDeleting(true); + // await deleteProfile(); + // await signOut(); + // await formbricksLogout(); + } catch (error) { + toast.error("Something went wrong"); + } finally { + setDeleting(false); + setOpen(false); + } + }; + + return ( + deleteTeam()} + text="Before you proceed with deleting this team, please be aware of the following consequences:" + isDeleting={deleting} + disabled={inputValue !== teamData.name}> +
+
    +
  • + Permanent removal of all products linked to this team. This includes all surveys, + responses, user actions and attributes associated with these products. +
  • +
  • + If you are the owner of a team with other admins, the ownership of that team will be transferred + to another admin. +
  • +
  • + If you are the only member of a team or there is no other admin present, the team will be + irreversibly deleted along with all associated data. +
  • +
  • This action cannot be undone. If it's gone, it's gone.
  • +
+
+ + +
+
+
+ ); +} diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/page.tsx index 83a3524346..27fc20aab8 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/page.tsx @@ -2,6 +2,7 @@ import SettingsCard from "../SettingsCard"; import SettingsTitle from "../SettingsTitle"; import { EditMemberships } from "./EditMemberships"; import EditTeamName from "./EditTeamName"; +import DeleteTeam from "./DeleteTeam"; export default function MembersSettingsPage({ params }) { return ( @@ -13,6 +14,11 @@ export default function MembersSettingsPage({ params }) { + + + ); } diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/profile/editProfile.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/profile/editProfile.tsx index 193d49abc6..42aeaef46d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/profile/editProfile.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/profile/editProfile.tsx @@ -76,13 +76,13 @@ export function EditAvatar({ session }) { ); } -interface DeleteAccounModaltProps { +interface DeleteAccountModalProps { open: boolean; setOpen: Dispatch>; session: Session; } -function DeleteAccountModal({ setOpen, open, session }: DeleteAccounModaltProps) { +function DeleteAccountModal({ setOpen, open, session }: DeleteAccountModalProps) { const [deleting, setDeleting] = useState(false); const [inputValue, setInputValue] = useState(""); diff --git a/apps/web/pages/api/v1/environments/[environmentId]/team/index.ts b/apps/web/pages/api/v1/environments/[environmentId]/team/index.ts index 3af4f870b7..9a126c0fc1 100644 --- a/apps/web/pages/api/v1/environments/[environmentId]/team/index.ts +++ b/apps/web/pages/api/v1/environments/[environmentId]/team/index.ts @@ -43,7 +43,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) }); if (team === null) { - return res.status(404).json({ message: "This product doesn't exist" }); + return res.status(404).json({ message: "This team doesn't exist" }); } return res.json(team); } From c3f26f7ab8293f0343b7f63cc67fee1f00437c7c Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Sat, 5 Aug 2023 09:32:23 +0530 Subject: [PATCH 02/10] feat: added delete team functionality --- .../settings/members/DeleteTeam.tsx | 107 +++++++----------- apps/web/lib/teams/teams.ts | 7 ++ .../[environmentId]/team/index.ts | 65 ++++++++++- pnpm-lock.yaml | 56 ++++----- 4 files changed, 141 insertions(+), 94 deletions(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx index 34a51e4ac0..4637e4df85 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx @@ -1,61 +1,56 @@ "use client"; +import toast from "react-hot-toast"; import DeleteDialog from "@/components/shared/DeleteDialog"; import LoadingSpinner from "@/components/shared/LoadingSpinner"; -import { useEnvironment } from "@/lib/environments/environments"; +import { useState, Dispatch, SetStateAction } from "react"; +import { useRouter } from "next/navigation"; import { useMembers } from "@/lib/members"; -import { useProduct } from "@/lib/products/products"; import { useProfile } from "@/lib/profile"; import { truncate } from "@/lib/utils"; import { Button, ErrorComponent, Input } from "@formbricks/ui"; -import { useTeamMutation } from "@/lib/teams/mutateTeams"; -import { useTeam } from "@/lib/teams/teams"; -import { useState, Dispatch, SetStateAction } from "react"; -import toast from "react-hot-toast"; +import { useTeam, deleteTeam } from "@/lib/teams/teams"; import { useMemberships } from "@/lib/memberships"; export default function DeleteTeam({ environmentId }) { const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const router = useRouter(); const { profile } = useProfile(); const { memberships } = useMemberships(); - const { team } = useMembers(environmentId); - const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId); - const { environment } = useEnvironment(environmentId); - const { team: teamData } = useTeam(environmentId); - - console.log({ profile, team, product, environment, teamData, memberships }); + const { team, isErrorTeam: isErrorTeamMembers } = useMembers(environmentId); + const { team: teamData, isLoadingTeam, isErrorTeam } = useTeam(environmentId); const availableTeams = memberships?.length; const role = team?.members?.filter((member) => member?.userId === profile?.id)[0]?.role; - const isUserAdminOrOwner = role === "admin" || role === "owner"; - const isDeleteDisabled = availableTeams <= 1 || !isUserAdminOrOwner; + const isUserOwner = role === "owner"; + const isDeleteDisabled = availableTeams <= 1 || !isUserOwner; - if (isLoadingProduct) { + if (isLoadingTeam) { return ; } - if (isErrorProduct) { + if (isErrorTeam) { return ; } - // const handleDeleteProduct = async () => { - // if (environment?.availableProducts?.length <= 1) { - // toast.error("Cannot delete product. Your team needs at least 1."); - // setIsDeleteDialogOpen(false); - // return; - // } - // const deleteProductRes = await deleteProduct(environmentId); + const handleDeleteTeam = async () => { + if (memberships?.length <= 1) { + toast.error("Cannot delete team. You need at least 1."); + setIsDeleteDialogOpen(false); + return; + } + const deleteTeamRes = await deleteTeam(environmentId); - // if (deleteProductRes?.id?.length > 0) { - // toast.success("Product deleted successfully."); - // // router.push("/"); - // } else if (deleteProductRes?.message?.length > 0) { - // toast.error(deleteProductRes.message); - // setIsDeleteDialogOpen(false); - // } else { - // toast.error("Error deleting product. Please try again."); - // } - // }; + if (deleteTeamRes?.deletedTeam?.id?.length > 0) { + toast.success("Team deleted successfully."); + router.push("/"); + } else if (deleteTeamRes?.message?.length > 0) { + toast.error(deleteTeamRes.message); + setIsDeleteDialogOpen(false); + } else { + toast.error("Error deleting team. Please try again."); + } + }; return (
@@ -77,12 +72,17 @@ export default function DeleteTeam({ environmentId }) { )} {isDeleteDisabled && (

- {!isUserAdminOrOwner - ? "Only Admin or Owners can delete teams." + {!isUserOwner + ? "Only Owner can delete the team." : "This is your only team, it cannot be deleted. Create a new team first."}

)} - +
); } @@ -91,64 +91,41 @@ interface DeleteTeamModalProps { open: boolean; setOpen: Dispatch>; teamData: { name: string; id: string; plan: string }; + deleteTeam: () => void; } -function DeleteTeamModal({ setOpen, open, teamData }: DeleteTeamModalProps) { - const [deleting, setDeleting] = useState(false); +function DeleteTeamModal({ setOpen, open, teamData, deleteTeam }: DeleteTeamModalProps) { const [inputValue, setInputValue] = useState(""); const handleInputChange = (e) => { setInputValue(e.target.value); }; - const deleteTeam = async () => { - try { - setDeleting(true); - // await deleteProfile(); - // await signOut(); - // await formbricksLogout(); - } catch (error) { - toast.error("Something went wrong"); - } finally { - setDeleting(false); - setOpen(false); - } - }; - return ( deleteTeam()} + onDelete={deleteTeam} text="Before you proceed with deleting this team, please be aware of the following consequences:" - isDeleting={deleting} - disabled={inputValue !== teamData.name}> + disabled={inputValue !== teamData?.name}>
  • Permanent removal of all products linked to this team. This includes all surveys, responses, user actions and attributes associated with these products.
  • -
  • - If you are the owner of a team with other admins, the ownership of that team will be transferred - to another admin. -
  • -
  • - If you are the only member of a team or there is no other admin present, the team will be - irreversibly deleted along with all associated data. -
  • This action cannot be undone. If it's gone, it's gone.
{ mutateTeam: mutate, }; }; + +export const deleteTeam = async (environmentId: string) => { + const response = await fetch(`/api/v1/environments/${environmentId}/team/`, { + method: "DELETE", + }); + return response.json(); +}; diff --git a/apps/web/pages/api/v1/environments/[environmentId]/team/index.ts b/apps/web/pages/api/v1/environments/[environmentId]/team/index.ts index 9a126c0fc1..27e63f69db 100644 --- a/apps/web/pages/api/v1/environments/[environmentId]/team/index.ts +++ b/apps/web/pages/api/v1/environments/[environmentId]/team/index.ts @@ -1,8 +1,12 @@ -import { hasEnvironmentAccess } from "@/lib/api/apiHelper"; +import { getSessionUser, hasEnvironmentAccess } from "@/lib/api/apiHelper"; import { prisma } from "@formbricks/database"; import type { NextApiRequest, NextApiResponse } from "next"; export default async function handle(req: NextApiRequest, res: NextApiResponse) { + const currentUser: any = await getSessionUser(req, res); + if (!currentUser) { + return res.status(401).json({ message: "Not authenticated" }); + } const environmentId = req.query?.environmentId?.toString(); if (!environmentId) { @@ -47,6 +51,65 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) } return res.json(team); } + + // DELETE + else if (req.method === "DELETE") { + try { + const environment = await prisma.environment.findUnique({ + where: { + id: environmentId, + }, + select: { + product: { + select: { + teamId: true, + }, + }, + }, + }); + if (environment === null) { + return res.status(404).json({ message: "This environment doesn't exist" }); + } + const team = await prisma.team.findUnique({ + where: { + id: environment.product.teamId, + }, + select: { + id: true, + name: true, + stripeCustomerId: true, + plan: true, + }, + }); + + if (team === null) { + return res.status(404).json({ message: "This team doesn't exist" }); + } + + const membership = await prisma.membership.findUnique({ + where: { + userId_teamId: { + userId: currentUser.id, + teamId: team.id, + }, + }, + }); + + if (membership?.role !== "owner") { + return res.status(403).json({ message: "You are not allowed to delete this team" }); + } + + const prismaRes = await prisma.team.delete({ + where: { + id: team.id, + }, + }); + + return res.status(200).json({ deletedTeam: prismaRes }); + } catch (error) { + return res.status(500).json({ message: error.message }); + } + } // Unknown HTTP Method else { throw new Error(`The HTTP ${req.method} method is not supported by this route.`); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afe058769e..ec67ede3c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -19,7 +19,7 @@ importers: version: 3.12.7 turbo: specifier: latest - version: 1.10.7 + version: 1.10.3 apps/demo: dependencies: @@ -352,7 +352,7 @@ importers: version: 8.9.0(eslint@8.46.0) eslint-config-turbo: specifier: latest - version: 1.8.8(eslint@8.46.0) + version: 1.10.3(eslint@8.46.0) eslint-plugin-react: specifier: 7.33.1 version: 7.33.1(eslint@8.46.0) @@ -9808,13 +9808,13 @@ packages: eslint: 8.46.0 dev: true - /eslint-config-turbo@1.8.8(eslint@8.46.0): - resolution: {integrity: sha512-+yT22sHOT5iC1sbBXfLIdXfbZuiv9bAyOXsxTxFCWelTeFFnANqmuKB3x274CFvf7WRuZ/vYP/VMjzU9xnFnxA==} + /eslint-config-turbo@1.10.3(eslint@8.46.0): + resolution: {integrity: sha512-ggzPfTJfMsMS383oZ4zfTP1zQvyMyiigOQJRUnLt1nqII6SKkTzdKZdwmXRDHU24KFwUfEFtT6c8vnm2VhL0uQ==} peerDependencies: eslint: '>6.6.0' dependencies: eslint: 8.46.0 - eslint-plugin-turbo: 1.8.8(eslint@8.46.0) + eslint-plugin-turbo: 1.10.3(eslint@8.46.0) dev: true /eslint-import-resolver-node@0.3.6: @@ -10002,8 +10002,8 @@ packages: semver: 6.3.1 string.prototype.matchall: 4.0.8 - /eslint-plugin-turbo@1.8.8(eslint@8.46.0): - resolution: {integrity: sha512-zqyTIvveOY4YU5jviDWw9GXHd4RiKmfEgwsjBrV/a965w0PpDwJgEUoSMB/C/dU310Sv9mF3DSdEjxjJLaw6rA==} + /eslint-plugin-turbo@1.10.3(eslint@8.46.0): + resolution: {integrity: sha512-g3Mnnk7el1FqxHfqbE/MayLvCsYjA/vKmAnUj66kV4AlM7p/EZqdt42NMcMSKtDVEm0w+utQkkzWG2Xsa0Pd/g==} peerDependencies: eslint: '>6.6.0' dependencies: @@ -19763,65 +19763,65 @@ packages: dependencies: safe-buffer: 5.2.1 - /turbo-darwin-64@1.10.7: - resolution: {integrity: sha512-N2MNuhwrl6g7vGuz4y3fFG2aR1oCs0UZ5HKl8KSTn/VC2y2YIuLGedQ3OVbo0TfEvygAlF3QGAAKKtOCmGPNKA==} + /turbo-darwin-64@1.10.3: + resolution: {integrity: sha512-IIB9IomJGyD3EdpSscm7Ip1xVWtYb7D0x7oH3vad3gjFcjHJzDz9xZ/iw/qItFEW+wGFcLSRPd+1BNnuLM8AsA==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@1.10.7: - resolution: {integrity: sha512-WbJkvjU+6qkngp7K4EsswOriO3xrNQag7YEGRtfLoDdMTk4O4QTeU6sfg2dKfDsBpTidTvEDwgIYJhYVGzrz9Q==} + /turbo-darwin-arm64@1.10.3: + resolution: {integrity: sha512-SBNmOZU9YEB0eyNIxeeQ+Wi0Ufd+nprEVp41rgUSRXEIpXjsDjyBnKnF+sQQj3+FLb4yyi/yZQckB+55qXWEsw==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@1.10.7: - resolution: {integrity: sha512-x1CF2CDP1pDz/J8/B2T0hnmmOQI2+y11JGIzNP0KtwxDM7rmeg3DDTtDM/9PwGqfPotN9iVGgMiMvBuMFbsLhg==} + /turbo-linux-64@1.10.3: + resolution: {integrity: sha512-kvAisGKE7xHJdyMxZLvg53zvHxjqPK1UVj4757PQqtx9dnjYHSc8epmivE6niPgDHon5YqImzArCjVZJYpIGHQ==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@1.10.7: - resolution: {integrity: sha512-JtnBmaBSYbs7peJPkXzXxsRGSGBmBEIb6/kC8RRmyvPAMyqF8wIex0pttsI+9plghREiGPtRWv/lfQEPRlXnNQ==} + /turbo-linux-arm64@1.10.3: + resolution: {integrity: sha512-Qgaqln0IYRgyL0SowJOi+PNxejv1I2xhzXOI+D+z4YHbgSx87ox1IsALYBlK8VRVYY8VCXl+PN12r1ioV09j7A==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@1.10.7: - resolution: {integrity: sha512-7A/4CByoHdolWS8dg3DPm99owfu1aY/W0V0+KxFd0o2JQMTQtoBgIMSvZesXaWM57z3OLsietFivDLQPuzE75w==} + /turbo-windows-64@1.10.3: + resolution: {integrity: sha512-rbH9wManURNN8mBnN/ZdkpUuTvyVVEMiUwFUX4GVE5qmV15iHtZfDLUSGGCP2UFBazHcpNHG1OJzgc55GFFrUw==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@1.10.7: - resolution: {integrity: sha512-D36K/3b6+hqm9IBAymnuVgyePktwQ+F0lSXr2B9JfAdFPBktSqGmp50JNC7pahxhnuCLj0Vdpe9RqfnJw5zATA==} + /turbo-windows-arm64@1.10.3: + resolution: {integrity: sha512-ThlkqxhcGZX39CaTjsHqJnqVe+WImjX13pmjnpChz6q5HHbeRxaJSFzgrHIOt0sUUVx90W/WrNRyoIt/aafniw==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@1.10.7: - resolution: {integrity: sha512-xm0MPM28TWx1e6TNC3wokfE5eaDqlfi0G24kmeHupDUZt5Wd0OzHFENEHMPqEaNKJ0I+AMObL6nbSZonZBV2HA==} + /turbo@1.10.3: + resolution: {integrity: sha512-U4gKCWcKgLcCjQd4Pl8KJdfEKumpyWbzRu75A6FCj6Ctea1PIm58W6Ltw1QXKqHrl2pF9e1raAskf/h6dlrPCA==} hasBin: true requiresBuild: true optionalDependencies: - turbo-darwin-64: 1.10.7 - turbo-darwin-arm64: 1.10.7 - turbo-linux-64: 1.10.7 - turbo-linux-arm64: 1.10.7 - turbo-windows-64: 1.10.7 - turbo-windows-arm64: 1.10.7 + turbo-darwin-64: 1.10.3 + turbo-darwin-arm64: 1.10.3 + turbo-linux-64: 1.10.3 + turbo-linux-arm64: 1.10.3 + turbo-windows-64: 1.10.3 + turbo-windows-arm64: 1.10.3 dev: true /tween-functions@1.2.0: From ccb89548f02102b96e87169196b41f93380171dd Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Sat, 5 Aug 2023 17:49:06 +0530 Subject: [PATCH 03/10] feat: added leave team --- .../settings/members/EditMemberships.tsx | 41 +++++++++++++++++++ apps/web/components/shared/DeleteDialog.tsx | 10 +++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships.tsx index 562a9426c1..e2f825dd95 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships.tsx @@ -36,6 +36,8 @@ import { PaperAirplaneIcon, ShareIcon, TrashIcon } from "@heroicons/react/24/out import { useState } from "react"; import toast from "react-hot-toast"; import AddMemberModal from "./AddMemberModal"; +import { useRouter } from "next/navigation"; +import { useMemberships } from "@/lib/memberships"; type EditMembershipsProps = { environmentId: string; @@ -128,14 +130,21 @@ export function EditMemberships({ environmentId }: EditMembershipsProps) { const [isDeleteMemberModalOpen, setDeleteMemberModalOpen] = useState(false); const [isCreateTeamModalOpen, setCreateTeamModalOpen] = useState(false); const [showShareInviteModal, setShowShareInviteModal] = useState(false); + const [isLeaveTeamModalOpen, setLeaveTeamModalOpen] = useState(false); const [shareInviteToken, setShareInviteToken] = useState(""); const [activeMember, setActiveMember] = useState({} as any); const { profile } = useProfile(); + const { memberships } = useMemberships(); + + const router = useRouter(); const role = team?.members?.filter((member) => member?.userId === profile?.id)[0]?.role; const isAdminOrOwner = role === "admin" || role === "owner"; + const availableTeams = memberships?.length; + const isLeaveTeamDisabled = availableTeams <= 1; + const handleOpenDeleteMemberModal = (e, member) => { e.preventDefault(); setActiveMember(member); @@ -194,9 +203,26 @@ export function EditMemberships({ environmentId }: EditMembershipsProps) { return now > expiresAt; }; + const handleLeaveTeam = async () => { + const result = await removeMember(team.teamId, profile?.id); + if (!result) { + toast.error("Something went wrong"); + } else { + toast.success("You left the team successfully"); + router.push("/"); + } + }; + + console.log(profile, memberships); + return ( <>
+ {role !== "owner" && ( + + )}
From 8d7eeb045b8b563d90d9dbb0006989b837ec7e1e Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Sun, 6 Aug 2023 14:58:52 +0530 Subject: [PATCH 04/10] feat: added transfer ownership --- .../settings/members/DeleteTeam.tsx | 2 +- .../settings/members/EditMemberships.tsx | 116 ++++++++++++------ .../members/TransferOwnershipModal.tsx | 58 +++++++++ apps/web/components/shared/CustomDialog.tsx | 54 ++++++++ apps/web/components/shared/DeleteDialog.tsx | 10 +- apps/web/lib/api/apiHelper.ts | 15 +++ apps/web/lib/members.ts | 14 +++ .../[teamId]/transfer-ownership/index.ts | 83 +++++++++++++ 8 files changed, 309 insertions(+), 43 deletions(-) create mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/members/TransferOwnershipModal.tsx create mode 100644 apps/web/components/shared/CustomDialog.tsx create mode 100644 apps/web/pages/api/v1/teams/[teamId]/transfer-ownership/index.ts diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx index 4637e4df85..d4168ad66b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx @@ -18,7 +18,7 @@ export default function DeleteTeam({ environmentId }) { const router = useRouter(); const { profile } = useProfile(); const { memberships } = useMemberships(); - const { team, isErrorTeam: isErrorTeamMembers } = useMembers(environmentId); + const { team } = useMembers(environmentId); const { team: teamData, isLoadingTeam, isErrorTeam } = useTeam(environmentId); const availableTeams = memberships?.length; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships.tsx index e2f825dd95..52ba615704 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships.tsx @@ -1,6 +1,7 @@ "use client"; import ShareInviteModal from "@/app/(app)/environments/[environmentId]/settings/members/ShareInviteModal"; +import TransferOwnershipModal from "@/app/(app)/environments/[environmentId]/settings/members/TransferOwnershipModal"; import DeleteDialog from "@/components/shared/DeleteDialog"; import LoadingSpinner from "@/components/shared/LoadingSpinner"; import CreateTeamModal from "@/components/team/CreateTeamModal"; @@ -11,6 +12,7 @@ import { removeMember, resendInvite, shareInvite, + transferOwnership, updateInviteeRole, updateMemberRole, useMembers, @@ -38,6 +40,7 @@ import toast from "react-hot-toast"; import AddMemberModal from "./AddMemberModal"; import { useRouter } from "next/navigation"; import { useMemberships } from "@/lib/memberships"; +import CustomDialog from "@/components/shared/CustomDialog"; type EditMembershipsProps = { environmentId: string; @@ -48,13 +51,16 @@ interface Role { memberRole: MembershipRole; teamId: string; memberId: string; + memberName: string; environmentId: string; userId: string; memberAccepted: boolean; inviteId: string; + currentUserRole: string; } enum MembershipRole { + Owner = "owner", Admin = "admin", Editor = "editor", Developer = "developer", @@ -66,13 +72,16 @@ function RoleElement({ memberRole, teamId, memberId, + memberName, environmentId, userId, memberAccepted, inviteId, + currentUserRole, }: Role) { const { mutateTeam } = useMembers(environmentId); const [loading, setLoading] = useState(false); + const [isTransferOwnershipModalOpen, setTransferOwnershipModalOpen] = useState(false); const disableRole = memberRole && memberId && userId ? memberRole === ("owner" as MembershipRole) || memberId === userId @@ -89,34 +98,70 @@ function RoleElement({ mutateTeam(); }; + const handleOwnershipTransfer = async () => { + setLoading(true); + const isTransfered = await transferOwnership(teamId, memberId); + if (isTransfered) { + toast.success("Ownership transferred successfully"); + } else { + toast.error("Something went wrong"); + } + setTransferOwnershipModalOpen(false); + setLoading(false); + mutateTeam(); + }; + + const handleRoleChange = (role: string) => { + if (role === "owner") { + setTransferOwnershipModalOpen(true); + } else { + handleMemberRoleUpdate(role); + } + }; + + const getMembershipRoles = () => { + if (currentUserRole === "owner" && memberAccepted) { + return Object.keys(MembershipRole); + } + return Object.keys(MembershipRole).filter((role) => role !== "Owner"); + }; + if (isAdminOrOwner) { return ( - - - - - {!disableRole && ( - - handleMemberRoleUpdate(value.toLowerCase())}> - {Object.keys(MembershipRole).map((role) => ( - - {capitalizeFirstLetter(role)} - - ))} - - - )} - + <> + + + + + {!disableRole && ( + + handleRoleChange(value.toLowerCase())}> + {getMembershipRoles().map((role) => ( + + {capitalizeFirstLetter(role)} + + ))} + + + )} + + + ); } @@ -213,8 +258,6 @@ export function EditMemberships({ environmentId }: EditMembershipsProps) { } }; - console.log(profile, memberships); - return ( <>
@@ -268,11 +311,13 @@ export function EditMemberships({ environmentId }: EditMembershipsProps) { isAdminOrOwner={isAdminOrOwner} memberRole={member.role} memberId={member.userId} + memberName={member.name} teamId={team.teamId} environmentId={environmentId} userId={profile?.id} memberAccepted={member.accepted} inviteId={member?.inviteId} + currentUserRole={role} />
@@ -336,21 +381,22 @@ export function EditMemberships({ environmentId }: EditMembershipsProps) { deleteWhat={activeMember.name + " from your team"} onDelete={handleDeleteMember} /> - {isLeaveTeamDisabled && (

You cannot leave this team as it is your only team. Create a new team first.

)} -
+ + {showShareInviteModal && ( >; + memberName: string; + onSubmit: () => void; +} + +export default function TransferOwnershipModal({ + setOpen, + open, + memberName, + onSubmit, +}: TransferOwnershipModalProps) { + const [inputValue, setInputValue] = useState(""); + + const handleInputChange = (e) => { + setInputValue(e.target.value); + }; + + return ( + +
+
    +
  • + There can only be one owner of each team. If you transfer your ownership to {memberName}, + you will lose all of your ownership rights. +
  • +
  • When you transfer the ownership, you will remain an Admin of the team.
  • +
+ + + + +
+
+ ); +} diff --git a/apps/web/components/shared/CustomDialog.tsx b/apps/web/components/shared/CustomDialog.tsx new file mode 100644 index 0000000000..0bec931d45 --- /dev/null +++ b/apps/web/components/shared/CustomDialog.tsx @@ -0,0 +1,54 @@ +"use client"; + +import Modal from "@/components/shared/Modal"; +import { Button } from "@formbricks/ui"; + +interface CustomDialogProps { + open: boolean; + setOpen: (open: boolean) => void; + title?: string; + text?: string; + isLoading?: boolean; + children?: React.ReactNode; + onOk: () => void; + okBtnText?: string; + onCancel?: () => void; + cancelBtnText?: string; + disabled?: boolean; +} + +export default function CustomDialog({ + open, + setOpen, + title, + text, + isLoading, + children, + onOk, + okBtnText, + onCancel, + cancelBtnText, + disabled, +}: CustomDialogProps) { + return ( + +

{text}

+
{children}
+
+ + +
+
+ ); +} diff --git a/apps/web/components/shared/DeleteDialog.tsx b/apps/web/components/shared/DeleteDialog.tsx index edf8f10436..8fb8f4ff77 100644 --- a/apps/web/components/shared/DeleteDialog.tsx +++ b/apps/web/components/shared/DeleteDialog.tsx @@ -6,22 +6,19 @@ import { Button } from "@formbricks/ui"; interface DeleteDialogProps { open: boolean; setOpen: (open: boolean) => void; - customTitle?: string; - deleteWhat?: string; + deleteWhat: string; onDelete: () => void; text?: string; isDeleting?: boolean; useSaveInsteadOfCancel?: boolean; onSave?: () => void; children?: React.ReactNode; - deleteBtnText?: string; disabled?: boolean; } export default function DeleteDialog({ open, setOpen, - customTitle, deleteWhat, onDelete, text, @@ -29,11 +26,10 @@ export default function DeleteDialog({ useSaveInsteadOfCancel = false, onSave, children, - deleteBtnText, disabled, }: DeleteDialogProps) { return ( - +

{text || "Are you sure? This action cannot be undone."}

{children}
@@ -48,7 +44,7 @@ export default function DeleteDialog({ {useSaveInsteadOfCancel ? "Save" : "Cancel"}
diff --git a/apps/web/lib/api/apiHelper.ts b/apps/web/lib/api/apiHelper.ts index 1e4e22f52c..010ebe48a8 100644 --- a/apps/web/lib/api/apiHelper.ts +++ b/apps/web/lib/api/apiHelper.ts @@ -131,6 +131,21 @@ export const getSessionUser = async (req?: NextApiRequest, res?: NextApiResponse if (session && "user" in session) return session.user; }; +export const isOwner = async (user, teamId) => { + const membership = await prisma.membership.findUnique({ + where: { + userId_teamId: { + userId: user.id, + teamId: teamId, + }, + }, + }); + if (membership && membership.role === "owner") { + return true; + } + return false; +}; + export const isAdminOrOwner = async (user, teamId) => { const membership = await prisma.membership.findUnique({ where: { diff --git a/apps/web/lib/members.ts b/apps/web/lib/members.ts index 414dd1f901..b8d82abc70 100644 --- a/apps/web/lib/members.ts +++ b/apps/web/lib/members.ts @@ -30,6 +30,20 @@ export const updateMemberRole = async (teamId: string, userId: string, role: str } }; +export const transferOwnership = async (teamId: string, userId: string) => { + try { + const result = await fetch(`/api/v1/teams/${teamId}/transfer-ownership/`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userId }), + }); + return result.status === 200; + } catch (error) { + console.error(error); + return false; + } +}; + export const removeMember = async (teamId: string, userId: string) => { try { const result = await fetch(`/api/v1/teams/${teamId}/members/${userId}/`, { diff --git a/apps/web/pages/api/v1/teams/[teamId]/transfer-ownership/index.ts b/apps/web/pages/api/v1/teams/[teamId]/transfer-ownership/index.ts new file mode 100644 index 0000000000..32a64acb74 --- /dev/null +++ b/apps/web/pages/api/v1/teams/[teamId]/transfer-ownership/index.ts @@ -0,0 +1,83 @@ +import { getSessionUser, hasTeamAccess, isOwner } from "@/lib/api/apiHelper"; +import { prisma } from "@formbricks/database"; +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function handle(req: NextApiRequest, res: NextApiResponse) { + // Check Authentication + const currentUser: any = await getSessionUser(req, res); + if (!currentUser) { + return res.status(401).json({ message: "Not authenticated" }); + } + + const teamId = req.query.teamId?.toString(); + if (teamId === undefined) { + return res.status(400).json({ message: "Missing teamId" }); + } + + const hasAccess = await hasTeamAccess(currentUser, teamId); + if (!hasAccess) { + return res.status(403).json({ message: "Not authorized" }); + } + + /** + * Transfer ownership of a team to another member + * @route PATCH /api/v1/teams/{teamId}/transfer-ownership + * @param {string} teamId - The id of the team to transfer ownership of + * @param {string} userId - The id of the user to transfer ownership to + * @returns {object} - The updated membership of the new owner and the updated membership of the old owner + * + */ + if (req.method === "PATCH") { + const { userId: newOwnerId } = req.body; + + const hasOwnerAccess = await isOwner(currentUser, teamId); + + if (!hasOwnerAccess) { + return res.status(403).json({ message: "You must be the owner of this team to transfer ownership" }); + } + + if (newOwnerId === currentUser.id) { + return res.status(403).json({ message: "You cannot transfer ownership to yourself" }); + } + + const isMember = await prisma.membership.findFirst({ + where: { + userId: newOwnerId, + teamId, + }, + }); + if (!isMember) { + return res.status(403).json({ message: "The new owner must be a member of the team" }); + } + + const updatedOwnerMembership = await prisma.membership.update({ + where: { + userId_teamId: { + teamId, + userId: newOwnerId, + }, + }, + data: { + role: "owner", + }, + }); + + const updatedAdminMembership = await prisma.membership.update({ + where: { + userId_teamId: { + teamId, + userId: currentUser.id, + }, + }, + data: { + role: "admin", + }, + }); + + return res.json({ + message: "Ownership transferred successfully", + updatedOwnerMembership, + updatedAdminMembership, + }); + } +} From ad86c4dbf4590ffd9c8debb3489234356f6696b7 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Sun, 6 Aug 2023 15:17:08 +0530 Subject: [PATCH 05/10] fix: independent queries -> Txn --- .../[teamId]/transfer-ownership/index.ts | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/apps/web/pages/api/v1/teams/[teamId]/transfer-ownership/index.ts b/apps/web/pages/api/v1/teams/[teamId]/transfer-ownership/index.ts index 32a64acb74..d4afcecebc 100644 --- a/apps/web/pages/api/v1/teams/[teamId]/transfer-ownership/index.ts +++ b/apps/web/pages/api/v1/teams/[teamId]/transfer-ownership/index.ts @@ -50,34 +50,35 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) return res.status(403).json({ message: "The new owner must be a member of the team" }); } - const updatedOwnerMembership = await prisma.membership.update({ - where: { - userId_teamId: { - teamId, - userId: newOwnerId, - }, - }, - data: { - role: "owner", - }, - }); + try { + await prisma.$transaction([ + prisma.membership.update({ + where: { + userId_teamId: { + teamId, + userId: currentUser.id, + }, + }, + data: { + role: "admin", + }, + }), + prisma.membership.update({ + where: { + userId_teamId: { + teamId, + userId: newOwnerId, + }, + }, + data: { + role: "owner", + }, + }), + ]); + } catch (error) { + return res.status(500).json({ message: "Something went wrong" }); + } - const updatedAdminMembership = await prisma.membership.update({ - where: { - userId_teamId: { - teamId, - userId: currentUser.id, - }, - }, - data: { - role: "admin", - }, - }); - - return res.json({ - message: "Ownership transferred successfully", - updatedOwnerMembership, - updatedAdminMembership, - }); + return res.json({ message: "Ownership transferred successfully" }); } } From 37afd004af29f63b5cbc8971a31204b9860e4aef Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Sun, 6 Aug 2023 15:30:54 +0530 Subject: [PATCH 06/10] refactor: used isOwner in delete team --- .../environments/[environmentId]/team/index.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/apps/web/pages/api/v1/environments/[environmentId]/team/index.ts b/apps/web/pages/api/v1/environments/[environmentId]/team/index.ts index 27e63f69db..c0598a85b5 100644 --- a/apps/web/pages/api/v1/environments/[environmentId]/team/index.ts +++ b/apps/web/pages/api/v1/environments/[environmentId]/team/index.ts @@ -1,4 +1,4 @@ -import { getSessionUser, hasEnvironmentAccess } from "@/lib/api/apiHelper"; +import { getSessionUser, hasEnvironmentAccess, hasTeamAccess, isOwner } from "@/lib/api/apiHelper"; import { prisma } from "@formbricks/database"; import type { NextApiRequest, NextApiResponse } from "next"; @@ -70,6 +70,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) if (environment === null) { return res.status(404).json({ message: "This environment doesn't exist" }); } + const team = await prisma.team.findUnique({ where: { id: environment.product.teamId, @@ -81,21 +82,12 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) plan: true, }, }); - if (team === null) { return res.status(404).json({ message: "This team doesn't exist" }); } - const membership = await prisma.membership.findUnique({ - where: { - userId_teamId: { - userId: currentUser.id, - teamId: team.id, - }, - }, - }); - - if (membership?.role !== "owner") { + const hasOwnership = isOwner(currentUser, team.id); + if (!hasOwnership) { return res.status(403).json({ message: "You are not allowed to delete this team" }); } From 205593d8d34512347de8027afeef18946f954a5d Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Sun, 6 Aug 2023 15:46:11 +0530 Subject: [PATCH 07/10] added loading in transfer ownership CTA --- .../[environmentId]/settings/members/EditMemberships.tsx | 1 + .../settings/members/TransferOwnershipModal.tsx | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships.tsx index 52ba615704..b907c04efd 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships.tsx @@ -160,6 +160,7 @@ function RoleElement({ setOpen={setTransferOwnershipModalOpen} memberName={memberName} onSubmit={handleOwnershipTransfer} + isLoading={loading} /> ); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/TransferOwnershipModal.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/TransferOwnershipModal.tsx index 3f1a645669..fdd297e57e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/TransferOwnershipModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/TransferOwnershipModal.tsx @@ -7,6 +7,7 @@ interface TransferOwnershipModalProps { setOpen: Dispatch>; memberName: string; onSubmit: () => void; + isLoading?: boolean; } export default function TransferOwnershipModal({ @@ -14,6 +15,7 @@ export default function TransferOwnershipModal({ open, memberName, onSubmit, + isLoading, }: TransferOwnershipModalProps) { const [inputValue, setInputValue] = useState(""); @@ -29,7 +31,8 @@ export default function TransferOwnershipModal({ okBtnText="Transfer ownership" title="There can only be ONE owner! Are you sure?" cancelBtnText="CANCEL" - disabled={inputValue !== memberName}> + disabled={inputValue !== memberName} + isLoading={isLoading}>
  • From 7ebdf9939ec24194571af206b8dce743ad711ee4 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Tue, 8 Aug 2023 10:16:48 +0530 Subject: [PATCH 08/10] fix: added loading states in CTA --- .../settings/members/DeleteTeam.tsx | 20 +++++++++---------- .../settings/members/EditMemberships.tsx | 7 ++++++- .../[environmentId]/settings/members/page.tsx | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx index d4168ad66b..06b6865a78 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx @@ -7,13 +7,13 @@ import { useState, Dispatch, SetStateAction } from "react"; import { useRouter } from "next/navigation"; import { useMembers } from "@/lib/members"; import { useProfile } from "@/lib/profile"; -import { truncate } from "@/lib/utils"; import { Button, ErrorComponent, Input } from "@formbricks/ui"; import { useTeam, deleteTeam } from "@/lib/teams/teams"; import { useMemberships } from "@/lib/memberships"; export default function DeleteTeam({ environmentId }) { const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); const router = useRouter(); const { profile } = useProfile(); @@ -34,19 +34,16 @@ export default function DeleteTeam({ environmentId }) { } const handleDeleteTeam = async () => { - if (memberships?.length <= 1) { - toast.error("Cannot delete team. You need at least 1."); - setIsDeleteDialogOpen(false); - return; - } + setIsDeleting(true); const deleteTeamRes = await deleteTeam(environmentId); + setIsDeleteDialogOpen(false); + setIsDeleting(false); if (deleteTeamRes?.deletedTeam?.id?.length > 0) { toast.success("Team deleted successfully."); router.push("/"); } else if (deleteTeamRes?.message?.length > 0) { toast.error(deleteTeamRes.message); - setIsDeleteDialogOpen(false); } else { toast.error("Error deleting team. Please try again."); } @@ -57,8 +54,6 @@ export default function DeleteTeam({ environmentId }) { {!isDeleteDisabled && (

    - Delete {truncate(teamData?.name, 30)} -  with all its products incl. all surveys, responses, people, actions and attributes.{" "} This action cannot be undone.

    ); @@ -92,9 +88,10 @@ interface DeleteTeamModalProps { setOpen: Dispatch>; teamData: { name: string; id: string; plan: string }; deleteTeam: () => void; + isDeleting?: boolean; } -function DeleteTeamModal({ setOpen, open, teamData, deleteTeam }: DeleteTeamModalProps) { +function DeleteTeamModal({ setOpen, open, teamData, deleteTeam, isDeleting }: DeleteTeamModalProps) { const [inputValue, setInputValue] = useState(""); const handleInputChange = (e) => { @@ -108,7 +105,8 @@ function DeleteTeamModal({ setOpen, open, teamData, deleteTeam }: DeleteTeamModa deleteWhat="team" onDelete={deleteTeam} text="Before you proceed with deleting this team, please be aware of the following consequences:" - disabled={inputValue !== teamData?.name}> + disabled={inputValue !== teamData?.name} + isDeleting={isDeleting}>
    • diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships.tsx index b907c04efd..20bbe9e134 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships.tsx @@ -172,6 +172,7 @@ function RoleElement({ export function EditMemberships({ environmentId }: EditMembershipsProps) { const { team, isErrorTeam, isLoadingTeam, mutateTeam } = useMembers(environmentId); + const [loading, setLoading] = useState(false); const [isAddMemberModalOpen, setAddMemberModalOpen] = useState(false); const [isDeleteMemberModalOpen, setDeleteMemberModalOpen] = useState(false); const [isCreateTeamModalOpen, setCreateTeamModalOpen] = useState(false); @@ -250,7 +251,10 @@ export function EditMemberships({ environmentId }: EditMembershipsProps) { }; const handleLeaveTeam = async () => { + setLoading(true); const result = await removeMember(team.teamId, profile?.id); + setLeaveTeamModalOpen(false); + setLoading(false); if (!result) { toast.error("Something went wrong"); } else { @@ -390,7 +394,8 @@ export function EditMemberships({ environmentId }: EditMembershipsProps) { text="You wil leave this team and loose access to all surveys and responses. You can only rejoin if you are invited again." onOk={handleLeaveTeam} okBtnText="Yes, leave team" - disabled={isLeaveTeamDisabled}> + disabled={isLeaveTeamDisabled} + isLoading={loading}> {isLeaveTeamDisabled && (

      You cannot leave this team as it is your only team. Create a new team first. diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/page.tsx index 27fc20aab8..481ffb3dee 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/page.tsx @@ -16,7 +16,7 @@ export default function MembersSettingsPage({ params }) { + description="Delete team with all its products including all surveys, responses, people, actions and attributes">

    From a81ceff09e32f39bef68eccec9d5321a701d87e7 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 8 Aug 2023 10:33:26 +0200 Subject: [PATCH 09/10] update formatting --- .../[environmentId]/settings/members/DeleteTeam.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx index 06b6865a78..31ae560a53 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx @@ -54,7 +54,7 @@ export default function DeleteTeam({ environmentId }) { {!isDeleteDisabled && (

    - This action cannot be undone. + This action cannot be undone. If it's gone, it's gone.