From 14458ae20d654c737011aaa0976a8a8a7baa7510 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 14 Jul 2022 22:25:15 +0800 Subject: [PATCH] Refactor OwnerDropdown into its own component --- .../interfaces/Organization/OwnerDropdown.tsx | 211 ++++++++++++++++++ .../interfaces/Organization/index.ts | 9 +- studio/pages/org/[slug]/settings.tsx | 198 +--------------- 3 files changed, 223 insertions(+), 195 deletions(-) create mode 100644 studio/components/interfaces/Organization/OwnerDropdown.tsx diff --git a/studio/components/interfaces/Organization/OwnerDropdown.tsx b/studio/components/interfaces/Organization/OwnerDropdown.tsx new file mode 100644 index 00000000000..8b25c3ae564 --- /dev/null +++ b/studio/components/interfaces/Organization/OwnerDropdown.tsx @@ -0,0 +1,211 @@ +import { useContext, useState } from 'react' +import { observer } from 'mobx-react-lite' +import { timeout } from 'lib/helpers' +import { Button, IconMoreHorizontal, IconTrash, Dropdown } from '@supabase/ui' + +import { useOrganizationDetail, useStore } from 'hooks' +import { Member } from 'types' +import { API_URL } from 'lib/constants' +import { post, delete_ } from 'lib/common/fetch' +import { PageContext } from 'pages/org/[slug]/settings' +import TextConfirmModal from 'components/ui/Modals/TextConfirmModal' +import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal' + +const OwnerDropdown = observer(({ members, member }: any) => { + const { ui } = useStore() + const { mutateOrgMembers } = useOrganizationDetail(ui.selectedOrganization?.slug || '') + + const PageState: any = useContext(PageContext) + const [loading, setLoading] = useState(false) + + // handle modal visibility + const [ownerTransferIsVisble, setOwnerTransferIsVisble] = useState(false) + + const { id: orgId, slug: orgSlug, name: orgName } = PageState.organization + + // Invite is expired if older than 24hrs + function isInviteExpired(timestamp: Date) { + const inviteDate = new Date(timestamp) + const now = new Date() + var timeBetween = now.valueOf() - inviteDate.valueOf() + if (timeBetween / 1000 / 60 / 60 < 24) { + return true + } + } + + async function handleMemberDelete() { + await timeout(200) + + confirmAlert({ + title: 'Confirm to remove', + message: `This is permanent! Are you sure you want to remove ${member.profile.primary_email}?`, + onAsyncConfirm: async () => { + setLoading(true) + const response = await delete_(`${API_URL}/organizations/${orgSlug}/members/remove`, { + member_id: member.id, + }) + if (response.error) { + ui.setNotification({ + category: 'error', + message: `Failed to delete user: ${response.error.message}`, + }) + setLoading(false) + } else { + const updatedMembers = members.filter((x: any) => x.id !== member.id) + mutateOrgMembers(updatedMembers) + ui.setNotification({ category: 'success', message: 'Successfully removed member' }) + } + }, + }) + } + + async function handleTransfer() { + setLoading(true) + + const response = await post(`${API_URL}/organizations/${orgSlug}/transfer`, { + org_id: orgId, + member_id: member.id, + }) + if (response.error) { + ui.setNotification({ + category: 'error', + message: `Failed to transfer ownership: ${response.error.message}`, + }) + setLoading(false) + } else { + const updatedMembers = [...members] + const oldOwner = updatedMembers.find((x) => x.is_owner == true) + if (oldOwner) oldOwner.is_owner = false + const newOwner = updatedMembers.find((x) => x.id == member.id) + if (newOwner) newOwner.is_owner = true + mutateOrgMembers(updatedMembers) + setOwnerTransferIsVisble(false) + ui.setNotification({ category: 'success', message: 'Successfully transfered organization' }) + } + } + + async function handleResendInvite(member: Member) { + setLoading(true) + + const response = await post(`${API_URL}/organizations/${orgSlug}/members/invite`, { + invited_email: member.profile.primary_email, + owner_id: member.invited_id, + }) + + if (response.error) { + ui.setNotification({ + category: 'error', + message: `Failed to resend invitation: ${response.error.message}`, + }) + setLoading(false) + } else { + const updatedMembers = [...members] + mutateOrgMembers(updatedMembers) + ui.setNotification({ category: 'success', message: 'Resent the invitation.' }) + setLoading(false) + } + } + + async function handleRevokeInvitation(id: number) { + setLoading(true) + + const response = await delete_( + `${API_URL}/organizations/${orgSlug}/members/invite?invited_id=${id}`, + {} + ) + + if (response.error) { + ui.setNotification({ + category: 'error', + message: `Failed to revoke invitation: ${response.error.message}`, + }) + setLoading(false) + } else { + const updatedMembers = [...members] + mutateOrgMembers(updatedMembers) + ui.setNotification({ category: 'success', message: 'Successfully revoked the invitation.' }) + } + } + + return ( +
+ + {!member.invited_at && ( + setOwnerTransferIsVisble(!ownerTransferIsVisble)}> +
+

Make owner

+

Transfer ownership of "{orgName}"

+
+
+ )} + + {member.invited_at && ( + <> + handleRevokeInvitation(member.invited_id)}> +
+

Cancel invitation

+

Revoke this invitation.

+
+
+ + {!isInviteExpired(member.invited_at) && ( + <> + + handleResendInvite(member)}> +
+

Resend invitation

+

Invites expire after 24hrs.

+
+
+ + )} + + )} + + {!member.invited_at && ( + <> + + } onClick={handleMemberDelete}> + Remove member + + + )} + + } + > + +
+ + setOwnerTransferIsVisble(!ownerTransferIsVisble)} + onConfirm={handleTransfer} + alert="Payment methods such as credit cards will also be transferred. You may want to delete credit card information first before transferring." + text={ + + By transferring this organization, it will be solely owned by{' '} + {member.profile?.username}, they + will also be able to remove you from the organization as a member + + } + /> +
+ ) +}) + +export default OwnerDropdown diff --git a/studio/components/interfaces/Organization/index.ts b/studio/components/interfaces/Organization/index.ts index dba072cb192..683ac5fda3c 100644 --- a/studio/components/interfaces/Organization/index.ts +++ b/studio/components/interfaces/Organization/index.ts @@ -2,5 +2,12 @@ import BillingSettings from './BillingSettings/BillingSettings' import InvoicesSettings from './InvoicesSettings/InvoicesSettings' import InviteMemberModal from './InviteMemberModal' import DeleteOrganizationButton from './DeleteOrganizationButton' +import OwnerDropdown from './OwnerDropdown' -export { BillingSettings, InvoicesSettings, InviteMemberModal, DeleteOrganizationButton } +export { + BillingSettings, + InvoicesSettings, + InviteMemberModal, + DeleteOrganizationButton, + OwnerDropdown, +} diff --git a/studio/pages/org/[slug]/settings.tsx b/studio/pages/org/[slug]/settings.tsx index 7785da53126..d4270bff206 100644 --- a/studio/pages/org/[slug]/settings.tsx +++ b/studio/pages/org/[slug]/settings.tsx @@ -3,35 +3,32 @@ import { createContext, useEffect, useContext, useState } from 'react' import { useRouter } from 'next/router' import { observer, useLocalObservable } from 'mobx-react-lite' import { toJS } from 'mobx' -import { pluckJsonSchemaFields, pluckObjectFields, timeout } from 'lib/helpers' +import { pluckJsonSchemaFields, pluckObjectFields } from 'lib/helpers' import { AutoField } from 'uniforms-bootstrap4' import { organizations } from 'stores/jsonSchema' import { Loading, Button, Badge, - IconMoreHorizontal, Tabs, Typography, - IconTrash, Alert, Input, - Dropdown, IconSearch, IconUser, } from '@supabase/ui' import { useOrganizationDetail, useStore, withAuth } from 'hooks' -import { Member, NextPageWithLayout, Project } from 'types' +import { NextPageWithLayout, Project } from 'types' import { API_URL } from 'lib/constants' -import { post, delete_, patch } from 'lib/common/fetch' +import { post, patch } from 'lib/common/fetch' import Panel from 'components/ui/Panel' import { AccountLayoutWithoutAuth } from 'components/layouts' -import TextConfirmModal from 'components/ui/Modals/TextConfirmModal' import { BillingSettings, InvoicesSettings, + OwnerDropdown, InviteMemberModal, DeleteOrganizationButton, } from 'components/interfaces/Organization' @@ -453,190 +450,3 @@ const MembersView = observer(() => { ) }) - -const OwnerDropdown = observer(({ members, member }: any) => { - const { ui } = useStore() - const { mutateOrgMembers } = useOrganizationDetail(ui.selectedOrganization?.slug || '') - - const PageState: any = useContext(PageContext) - const [loading, setLoading] = useState(false) - - // handle modal visibility - const [ownerTransferIsVisble, setOwnerTransferIsVisble] = useState(false) - - const { id: orgId, slug: orgSlug, name: orgName } = PageState.organization - - async function handleMemberDelete() { - await timeout(200) - - confirmAlert({ - title: 'Confirm to remove', - message: `This is permanent! Are you sure you want to remove ${member.profile.primary_email}?`, - onAsyncConfirm: async () => { - setLoading(true) - const response = await delete_(`${API_URL}/organizations/${orgSlug}/members/remove`, { - member_id: member.id, - }) - if (response.error) { - ui.setNotification({ - category: 'error', - message: `Failed to delete user: ${response.error.message}`, - }) - setLoading(false) - } else { - const updatedMembers = members.filter((x: any) => x.id !== member.id) - mutateOrgMembers(updatedMembers) - ui.setNotification({ category: 'success', message: 'Successfully removed member' }) - } - }, - }) - } - - async function handleTransfer() { - setLoading(true) - - const response = await post(`${API_URL}/organizations/${orgSlug}/transfer`, { - org_id: orgId, - member_id: member.id, - }) - if (response.error) { - ui.setNotification({ - category: 'error', - message: `Failed to transfer ownership: ${response.error.message}`, - }) - setLoading(false) - } else { - const updatedMembers = [...members] - const oldOwner = updatedMembers.find((x) => x.is_owner == true) - if (oldOwner) oldOwner.is_owner = false - const newOwner = updatedMembers.find((x) => x.id == member.id) - if (newOwner) newOwner.is_owner = true - mutateOrgMembers(updatedMembers) - setOwnerTransferIsVisble(false) - ui.setNotification({ category: 'success', message: 'Successfully transfered organization' }) - } - } - - async function handleResendInvite(member: Member) { - setLoading(true) - - const response = await post(`${API_URL}/organizations/${orgSlug}/members/invite`, { - invited_email: member.profile.primary_email, - owner_id: member.invited_id, - }) - - if (response.error) { - ui.setNotification({ - category: 'error', - message: `Failed to resend invitation: ${response.error.message}`, - }) - setLoading(false) - } else { - const updatedMembers = [...members] - mutateOrgMembers(updatedMembers) - ui.setNotification({ category: 'success', message: 'Resent the invitation.' }) - setLoading(false) - } - } - - async function handleRevokeInvitation(id: number) { - setLoading(true) - - const response = await delete_( - `${API_URL}/organizations/${orgSlug}/members/invite?invited_id=${id}`, - {} - ) - - if (response.error) { - ui.setNotification({ - category: 'error', - message: `Failed to revoke invitation: ${response.error.message}`, - }) - setLoading(false) - } else { - const updatedMembers = [...members] - mutateOrgMembers(updatedMembers) - ui.setNotification({ category: 'success', message: 'Successfully revoked the invitation.' }) - } - } - - return ( -
- - {!member.invited_at && ( - setOwnerTransferIsVisble(!ownerTransferIsVisble)}> -
-

Make owner

-

Transfer ownership of "{orgName}"

-
-
- )} - - {member.invited_at && ( - <> - handleRevokeInvitation(member.invited_id)}> -
-

Cancel invitation

-

Revoke this invitation.

-
-
- - {!inviteExpired(member.invited_at) && ( - <> - - handleResendInvite(member)}> -
-

Resend invitation

-

Invites expire after 24hrs.

-
-
- - )} - - )} - - {!member.invited_at && ( - <> - - } onClick={handleMemberDelete}> - Remove member - - - )} - - } - > - -
- - setOwnerTransferIsVisble(!ownerTransferIsVisble)} - onConfirm={handleTransfer} - alert="Payment methods such as credit cards will also be transferred. You may want to delete credit card information first before transferring." - text={ - - By transferring this organization, it will be solely owned by{' '} - {member.profile?.username}, they - will also be able to remove you from the organization as a member - - } - /> -
- ) -})