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
-
- }
- />
-
- )
-})