mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 05:39:11 +03:00
Refactor OwnerDropdown into its own component
This commit is contained in:
1 parent
d6a52397b9
commit
14458ae20d
3 files changed
+223
-195
No files matched your search
@@ -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 (
|
||||
<div className="flex items-center justify-end">
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="end"
|
||||
overlay={
|
||||
<>
|
||||
{!member.invited_at && (
|
||||
<Dropdown.Item onClick={() => setOwnerTransferIsVisble(!ownerTransferIsVisble)}>
|
||||
<div className="flex flex-col">
|
||||
<p>Make owner</p>
|
||||
<p className="block opacity-50">Transfer ownership of "{orgName}"</p>
|
||||
</div>
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
|
||||
{member.invited_at && (
|
||||
<>
|
||||
<Dropdown.Item onClick={() => handleRevokeInvitation(member.invited_id)}>
|
||||
<div className="flex flex-col">
|
||||
<p>Cancel invitation</p>
|
||||
<p className="block opacity-50">Revoke this invitation.</p>
|
||||
</div>
|
||||
</Dropdown.Item>
|
||||
|
||||
{!isInviteExpired(member.invited_at) && (
|
||||
<>
|
||||
<Dropdown.Seperator />
|
||||
<Dropdown.Item onClick={() => handleResendInvite(member)}>
|
||||
<div className="flex flex-col">
|
||||
<p>Resend invitation</p>
|
||||
<p className="block opacity-50">Invites expire after 24hrs.</p>
|
||||
</div>
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!member.invited_at && (
|
||||
<>
|
||||
<Dropdown.Seperator />
|
||||
<Dropdown.Item icon={<IconTrash size="tiny" />} onClick={handleMemberDelete}>
|
||||
Remove member
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
as="span"
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
type="text"
|
||||
icon={<IconMoreHorizontal />}
|
||||
></Button>
|
||||
</Dropdown>
|
||||
|
||||
<TextConfirmModal
|
||||
title="Transfer organization"
|
||||
visible={ownerTransferIsVisble}
|
||||
confirmString={orgSlug}
|
||||
loading={loading}
|
||||
confirmLabel="I understand, transfer ownership"
|
||||
confirmPlaceholder="Type in name of orgnization"
|
||||
onCancel={() => 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={
|
||||
<span>
|
||||
By transferring this organization, it will be solely owned by{' '}
|
||||
<span className="font-medium dark:text-white">{member.profile?.username}</span>, they
|
||||
will also be able to remove you from the organization as a member
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default OwnerDropdown
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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(() => {
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
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 (
|
||||
<div className="flex items-center justify-end">
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="end"
|
||||
overlay={
|
||||
<>
|
||||
{!member.invited_at && (
|
||||
<Dropdown.Item onClick={() => setOwnerTransferIsVisble(!ownerTransferIsVisble)}>
|
||||
<div className="flex flex-col">
|
||||
<p>Make owner</p>
|
||||
<p className="block opacity-50">Transfer ownership of "{orgName}"</p>
|
||||
</div>
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
|
||||
{member.invited_at && (
|
||||
<>
|
||||
<Dropdown.Item onClick={() => handleRevokeInvitation(member.invited_id)}>
|
||||
<div className="flex flex-col">
|
||||
<p>Cancel invitation</p>
|
||||
<p className="block opacity-50">Revoke this invitation.</p>
|
||||
</div>
|
||||
</Dropdown.Item>
|
||||
|
||||
{!inviteExpired(member.invited_at) && (
|
||||
<>
|
||||
<Dropdown.Seperator />
|
||||
<Dropdown.Item onClick={() => handleResendInvite(member)}>
|
||||
<div className="flex flex-col">
|
||||
<p>Resend invitation</p>
|
||||
<p className="block opacity-50">Invites expire after 24hrs.</p>
|
||||
</div>
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!member.invited_at && (
|
||||
<>
|
||||
<Dropdown.Seperator />
|
||||
<Dropdown.Item icon={<IconTrash size="tiny" />} onClick={handleMemberDelete}>
|
||||
Remove member
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
as="span"
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
type="text"
|
||||
icon={<IconMoreHorizontal />}
|
||||
></Button>
|
||||
</Dropdown>
|
||||
|
||||
<TextConfirmModal
|
||||
title="Transfer organization"
|
||||
visible={ownerTransferIsVisble}
|
||||
confirmString={orgSlug}
|
||||
loading={loading}
|
||||
confirmLabel="I understand, transfer ownership"
|
||||
confirmPlaceholder="Type in name of orgnization"
|
||||
onCancel={() => 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={
|
||||
<span>
|
||||
By transferring this organization, it will be solely owned by{' '}
|
||||
<span className="font-medium dark:text-white">{member.profile?.username}</span>, they
|
||||
will also be able to remove you from the organization as a member
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
Reference in new issue
Block a user