ajout app
This commit is contained in:
136
Voltaserve/ui/src/components/file/sharing/index.tsx
Normal file
136
Voltaserve/ui/src/components/file/sharing/index.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Tag,
|
||||
} from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import FileAPI, { List } from '@/client/api/file'
|
||||
import GroupAPI from '@/client/api/group'
|
||||
import { geOwnerPermission } from '@/client/api/permission'
|
||||
import UserAPI from '@/client/api/user'
|
||||
import WorkspaceAPI from '@/client/api/workspace'
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hook'
|
||||
import { sharingModalDidClose } from '@/store/ui/files'
|
||||
import SharingGroups from './sharing-groups'
|
||||
import SharingUsers from './sharing-users'
|
||||
|
||||
export type FileSharingProps = {
|
||||
list: List
|
||||
}
|
||||
|
||||
const FileSharing = ({ list }: FileSharingProps) => {
|
||||
const { id } = useParams()
|
||||
const dispatch = useAppDispatch()
|
||||
const selection = useAppSelector((state) => state.ui.files.selection)
|
||||
const isModalOpen = useAppSelector((state) => state.ui.files.isShareModalOpen)
|
||||
const singleFile = useMemo(() => {
|
||||
if (selection.length === 1) {
|
||||
return list.data.find((e) => e.id === selection[0])
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}, [list.data, selection])
|
||||
const { data: workspace } = WorkspaceAPI.useGetById(id)
|
||||
const { data: users } = UserAPI.useList({
|
||||
organizationId: workspace?.organization.id,
|
||||
})
|
||||
const { data: groups } = GroupAPI.useList({
|
||||
organizationId: workspace?.organization.id,
|
||||
})
|
||||
const { data: userPermissions, mutate: mutateUserPermissions } =
|
||||
FileAPI.useGetUserPermissions(
|
||||
singleFile && geOwnerPermission(singleFile.permission)
|
||||
? singleFile.id
|
||||
: undefined,
|
||||
)
|
||||
const { data: groupPermissions, mutate: mutateGroupPermissions } =
|
||||
FileAPI.useGetGroupPermissions(
|
||||
singleFile && geOwnerPermission(singleFile.permission)
|
||||
? singleFile.id
|
||||
: undefined,
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="xl"
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => {
|
||||
dispatch(sharingModalDidClose())
|
||||
}}
|
||||
closeOnOverlayClick={false}
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
{selection.length > 1 ? (
|
||||
<ModalHeader>Sharing {selection.length} Items(s)</ModalHeader>
|
||||
) : (
|
||||
<ModalHeader>Sharing</ModalHeader>
|
||||
)}
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Tabs>
|
||||
<TabList className={cx('h-[40px]')}>
|
||||
<Tab>
|
||||
<div
|
||||
className={cx('flex', 'flex-row', 'items-center', 'gap-0.5')}
|
||||
>
|
||||
<span>People</span>
|
||||
{singleFile &&
|
||||
userPermissions &&
|
||||
userPermissions.length > 0 ? (
|
||||
<Tag className={cx('rounded-full')}>
|
||||
{userPermissions.length}
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<div
|
||||
className={cx('flex', 'flex-row', 'items-center', 'gap-0.5')}
|
||||
>
|
||||
<span>Groups</span>
|
||||
{singleFile &&
|
||||
groupPermissions &&
|
||||
groupPermissions.length > 0 ? (
|
||||
<Tag className={cx('rounded-full')}>
|
||||
{groupPermissions.length}
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<SharingUsers
|
||||
users={users?.data}
|
||||
permissions={userPermissions}
|
||||
mutateUserPermissions={mutateUserPermissions}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SharingGroups
|
||||
groups={groups?.data}
|
||||
permissions={groupPermissions}
|
||||
mutateGroupPermissions={mutateGroupPermissions}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileSharing
|
@ -0,0 +1,12 @@
|
||||
import { Skeleton } from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
|
||||
const SharingFormSkeleton = () => (
|
||||
<div className={cx('flex', 'flex-col', 'gap-1.5')}>
|
||||
<Skeleton className={cx('rounded-xl', 'w-[40px]')} />
|
||||
<Skeleton className={cx('rounded-xl', 'w-[40px]')} />
|
||||
<Skeleton className={cx('rounded-xl', 'w-[40px]')} />
|
||||
</div>
|
||||
)
|
||||
|
||||
export default SharingFormSkeleton
|
232
Voltaserve/ui/src/components/file/sharing/sharing-groups.tsx
Normal file
232
Voltaserve/ui/src/components/file/sharing/sharing-groups.tsx
Normal file
@ -0,0 +1,232 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
IconButton,
|
||||
Badge,
|
||||
Avatar,
|
||||
} from '@chakra-ui/react'
|
||||
import { KeyedMutator, useSWRConfig } from 'swr'
|
||||
import { Select } from 'chakra-react-select'
|
||||
import cx from 'classnames'
|
||||
import FileAPI, { GroupPermission, List } from '@/client/api/file'
|
||||
import { Group } from '@/client/api/group'
|
||||
import { geEditorPermission } from '@/client/api/permission'
|
||||
import WorkspaceAPI from '@/client/api/workspace'
|
||||
import GroupSelector from '@/components/common/group-selector'
|
||||
import useFileListSearchParams from '@/hooks/use-file-list-params'
|
||||
import { Spinner, Text } from '@/lib'
|
||||
import { IconAdd, IconCheck, IconDelete } from '@/lib'
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hook'
|
||||
import { sharingModalDidClose } from '@/store/ui/files'
|
||||
import reactSelectStyles from '@/styles/react-select'
|
||||
import SharingFormSkeleton from './sharing-form-skeleton'
|
||||
|
||||
export type SharingGroupsProps = {
|
||||
groups?: Group[]
|
||||
permissions?: GroupPermission[]
|
||||
mutateGroupPermissions: KeyedMutator<GroupPermission[]>
|
||||
}
|
||||
|
||||
const SharingGroups = ({
|
||||
groups,
|
||||
permissions,
|
||||
mutateGroupPermissions,
|
||||
}: SharingGroupsProps) => {
|
||||
const { mutate } = useSWRConfig()
|
||||
const { id, fileId } = useParams()
|
||||
const dispatch = useAppDispatch()
|
||||
const { data: workspace } = WorkspaceAPI.useGetById(id)
|
||||
const selection = useAppSelector((state) => state.ui.files.selection)
|
||||
const [isGrantLoading, setIsGrantLoading] = useState(false)
|
||||
const [permissionBeingRevoked, setPermissionBeingRevoked] = useState<string>()
|
||||
const [activeGroup, setActiveGroup] = useState<Group>()
|
||||
const [activePermission, setActivePermission] = useState<string>()
|
||||
const fileListSearchParams = useFileListSearchParams()
|
||||
const isSingleSelection = selection.length === 1
|
||||
|
||||
const handleGrantGroupPermission = useCallback(async () => {
|
||||
if (activeGroup && activePermission) {
|
||||
try {
|
||||
setIsGrantLoading(true)
|
||||
await FileAPI.grantGroupPermission({
|
||||
ids: selection,
|
||||
groupId: activeGroup.id,
|
||||
permission: activePermission,
|
||||
})
|
||||
await mutate<List>(`/files/${fileId}/list?${fileListSearchParams}`)
|
||||
if (isSingleSelection) {
|
||||
await mutateGroupPermissions()
|
||||
}
|
||||
setActiveGroup(undefined)
|
||||
setIsGrantLoading(false)
|
||||
if (!isSingleSelection) {
|
||||
dispatch(sharingModalDidClose())
|
||||
}
|
||||
} catch {
|
||||
setIsGrantLoading(false)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
fileId,
|
||||
selection,
|
||||
activeGroup,
|
||||
activePermission,
|
||||
isSingleSelection,
|
||||
fileListSearchParams,
|
||||
mutate,
|
||||
dispatch,
|
||||
mutateGroupPermissions,
|
||||
])
|
||||
|
||||
const handleRevokeGroupPermission = useCallback(
|
||||
async (permission: GroupPermission) => {
|
||||
try {
|
||||
setPermissionBeingRevoked(permission.id)
|
||||
await FileAPI.revokeGroupPermission({
|
||||
ids: selection,
|
||||
groupId: permission.group.id,
|
||||
})
|
||||
await mutate<List>(`/files/${fileId}/list?${fileListSearchParams}`)
|
||||
if (isSingleSelection) {
|
||||
await mutateGroupPermissions()
|
||||
}
|
||||
} finally {
|
||||
setPermissionBeingRevoked(undefined)
|
||||
}
|
||||
},
|
||||
[
|
||||
fileId,
|
||||
selection,
|
||||
isSingleSelection,
|
||||
fileListSearchParams,
|
||||
mutate,
|
||||
mutateGroupPermissions,
|
||||
],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={cx('flex', 'flex-col', 'gap-1.5')}>
|
||||
{!groups ? <SharingFormSkeleton /> : null}
|
||||
{groups && groups.length > 0 ? (
|
||||
<div className={cx('flex', 'flex-col', 'gap-1.5')}>
|
||||
<GroupSelector
|
||||
value={activeGroup}
|
||||
organizationId={workspace?.organization.id}
|
||||
onConfirm={(value) => setActiveGroup(value)}
|
||||
/>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'viewer', label: 'Viewer' },
|
||||
{ value: 'editor', label: 'Editor' },
|
||||
{ value: 'owner', label: 'Owner' },
|
||||
]}
|
||||
placeholder="Select Permission"
|
||||
selectedOptionStyle="check"
|
||||
chakraStyles={reactSelectStyles}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setActivePermission(e.value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
leftIcon={<IconCheck />}
|
||||
colorScheme="blue"
|
||||
isLoading={isGrantLoading}
|
||||
isDisabled={!activeGroup || !activePermission}
|
||||
onClick={() => handleGrantGroupPermission()}
|
||||
>
|
||||
Apply to Group
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{groups && groups.length === 0 ? (
|
||||
<div className={cx('flex', 'items-center', 'justify-center')}>
|
||||
<div className={cx('flex', 'flex-col', 'items-center', 'gap-1.5')}>
|
||||
<span>This organization has no groups.</span>
|
||||
{workspace &&
|
||||
geEditorPermission(workspace.organization.permission) ? (
|
||||
<Button
|
||||
as={Link}
|
||||
leftIcon={<IconAdd />}
|
||||
to={`/new/group?org=${workspace.organization.id}`}
|
||||
>
|
||||
New Group
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{isSingleSelection ? (
|
||||
<>
|
||||
<hr />
|
||||
{!permissions ? (
|
||||
<div className={cx('flex', 'items-center', 'justify-center')}>
|
||||
<Spinner />
|
||||
</div>
|
||||
) : null}
|
||||
{permissions && permissions.length === 0 ? (
|
||||
<div className={cx('flex', 'items-center', 'justify-center')}>
|
||||
<span>Not shared with any groups.</span>
|
||||
</div>
|
||||
) : null}
|
||||
{permissions && permissions.length > 0 ? (
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Group</Th>
|
||||
<Th>Permission</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{permissions.map((p) => (
|
||||
<Tr key={p.id}>
|
||||
<Td className={cx('p-1')}>
|
||||
<div
|
||||
className={cx(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'gap-1',
|
||||
)}
|
||||
>
|
||||
<Avatar
|
||||
name={p.group.name}
|
||||
size="sm"
|
||||
className={cx('w-[40px]', 'h-[40px]')}
|
||||
/>
|
||||
<Text noOfLines={1}>{p.group.name}</Text>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge>{p.permission}</Badge>
|
||||
</Td>
|
||||
<Td className={cx('text-end')}>
|
||||
<IconButton
|
||||
icon={<IconDelete />}
|
||||
colorScheme="red"
|
||||
aria-label=""
|
||||
isLoading={permissionBeingRevoked === p.id}
|
||||
onClick={() => handleRevokeGroupPermission(p)}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SharingGroups
|
253
Voltaserve/ui/src/components/file/sharing/sharing-users.tsx
Normal file
253
Voltaserve/ui/src/components/file/sharing/sharing-users.tsx
Normal file
@ -0,0 +1,253 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
IconButton,
|
||||
Badge,
|
||||
Avatar,
|
||||
} from '@chakra-ui/react'
|
||||
import { KeyedMutator, useSWRConfig } from 'swr'
|
||||
import { Select } from 'chakra-react-select'
|
||||
import cx from 'classnames'
|
||||
import FileAPI, { List, UserPermission } from '@/client/api/file'
|
||||
import { geEditorPermission } from '@/client/api/permission'
|
||||
import { User } from '@/client/api/user'
|
||||
import WorkspaceAPI from '@/client/api/workspace'
|
||||
import IdPUserAPI from '@/client/idp/user'
|
||||
import UserSelector from '@/components/common/user-selector'
|
||||
import useFileListSearchParams from '@/hooks/use-file-list-params'
|
||||
import { Spinner, Text } from '@/lib'
|
||||
import { IconCheck, IconDelete, IconPersonAdd } from '@/lib'
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hook'
|
||||
import { sharingModalDidClose } from '@/store/ui/files'
|
||||
import { inviteModalDidOpen } from '@/store/ui/organizations'
|
||||
import reactSelectStyles from '@/styles/react-select'
|
||||
import SharingFormSkeleton from './sharing-form-skeleton'
|
||||
|
||||
export type SharingUsersProps = {
|
||||
users?: User[]
|
||||
permissions?: UserPermission[]
|
||||
mutateUserPermissions: KeyedMutator<UserPermission[]>
|
||||
}
|
||||
|
||||
const SharingUsers = ({
|
||||
users,
|
||||
permissions,
|
||||
mutateUserPermissions,
|
||||
}: SharingUsersProps) => {
|
||||
const navigate = useNavigate()
|
||||
const { mutate } = useSWRConfig()
|
||||
const { id, fileId } = useParams()
|
||||
const dispatch = useAppDispatch()
|
||||
const { data: workspace } = WorkspaceAPI.useGetById(id)
|
||||
const selection = useAppSelector((state) => state.ui.files.selection)
|
||||
const [isGrantLoading, setIsGrantLoading] = useState(false)
|
||||
const [permissionBeingRevoked, setPermissionBeingRevoked] = useState<string>()
|
||||
const [activeUser, setActiveUser] = useState<User>()
|
||||
const [activePermission, setActivePermission] = useState<string>()
|
||||
const { data: user } = IdPUserAPI.useGet()
|
||||
const fileListSearchParams = useFileListSearchParams()
|
||||
const isSingleSelection = selection.length === 1
|
||||
|
||||
const handleGrantUserPermission = useCallback(async () => {
|
||||
if (!activeUser || !activePermission) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
setIsGrantLoading(true)
|
||||
await FileAPI.grantUserPermission({
|
||||
ids: selection,
|
||||
userId: activeUser.id,
|
||||
permission: activePermission,
|
||||
})
|
||||
await mutate<List>(`/files/${fileId}/list?${fileListSearchParams}`)
|
||||
if (isSingleSelection) {
|
||||
await mutateUserPermissions()
|
||||
}
|
||||
setActiveUser(undefined)
|
||||
setIsGrantLoading(false)
|
||||
if (!isSingleSelection) {
|
||||
dispatch(sharingModalDidClose())
|
||||
}
|
||||
} catch {
|
||||
setIsGrantLoading(false)
|
||||
}
|
||||
}, [
|
||||
fileId,
|
||||
selection,
|
||||
activeUser,
|
||||
activePermission,
|
||||
isSingleSelection,
|
||||
fileListSearchParams,
|
||||
mutate,
|
||||
dispatch,
|
||||
mutateUserPermissions,
|
||||
])
|
||||
|
||||
const handleRevokeUserPermission = useCallback(
|
||||
async (permission: UserPermission) => {
|
||||
try {
|
||||
setPermissionBeingRevoked(permission.id)
|
||||
await FileAPI.revokeUserPermission({
|
||||
ids: selection,
|
||||
userId: permission.user.id,
|
||||
})
|
||||
await mutate<List>(`/files/${fileId}/list?${fileListSearchParams}`)
|
||||
if (isSingleSelection) {
|
||||
await mutateUserPermissions()
|
||||
}
|
||||
} finally {
|
||||
setPermissionBeingRevoked(undefined)
|
||||
}
|
||||
},
|
||||
[
|
||||
fileId,
|
||||
selection,
|
||||
isSingleSelection,
|
||||
fileListSearchParams,
|
||||
mutate,
|
||||
mutateUserPermissions,
|
||||
],
|
||||
)
|
||||
|
||||
const handleInviteMembersClick = useCallback(async () => {
|
||||
if (workspace) {
|
||||
dispatch(inviteModalDidOpen())
|
||||
dispatch(sharingModalDidClose())
|
||||
navigate(`/organization/${workspace.organization.id}/member`)
|
||||
}
|
||||
}, [workspace, navigate, dispatch])
|
||||
|
||||
return (
|
||||
<div className={cx('flex', 'flex-col', 'gap-1.5')}>
|
||||
{!users ? <SharingFormSkeleton /> : null}
|
||||
{users && users.length === 0 ? (
|
||||
<div className={cx('flex', 'items-center', 'justify-center')}>
|
||||
<div className={cx('flex', 'flex-col', 'items-center', 'gap-1.5')}>
|
||||
<span>This organization has no members.</span>
|
||||
{workspace &&
|
||||
geEditorPermission(workspace.organization.permission) ? (
|
||||
<Button
|
||||
leftIcon={<IconPersonAdd />}
|
||||
onClick={handleInviteMembersClick}
|
||||
>
|
||||
Invite Members
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{users && users.length > 0 ? (
|
||||
<div className={cx('flex', 'flex-col', 'gap-1.5')}>
|
||||
<UserSelector
|
||||
value={activeUser}
|
||||
organizationId={workspace?.organization.id}
|
||||
onConfirm={(value) => setActiveUser(value)}
|
||||
/>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'viewer', label: 'Viewer' },
|
||||
{ value: 'editor', label: 'Editor' },
|
||||
{ value: 'owner', label: 'Owner' },
|
||||
]}
|
||||
placeholder="Select Permission"
|
||||
selectedOptionStyle="check"
|
||||
chakraStyles={reactSelectStyles}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setActivePermission(e.value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
leftIcon={<IconCheck />}
|
||||
colorScheme="blue"
|
||||
isLoading={isGrantLoading}
|
||||
isDisabled={!activeUser || !activePermission}
|
||||
onClick={() => handleGrantUserPermission()}
|
||||
>
|
||||
Apply to User
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{isSingleSelection ? (
|
||||
<>
|
||||
<hr />
|
||||
{!permissions ? (
|
||||
<div className={cx('flex', 'items-center', 'justify-center')}>
|
||||
<Spinner />
|
||||
</div>
|
||||
) : null}
|
||||
{permissions && permissions.length === 0 ? (
|
||||
<div className={cx('flex', 'items-center', 'justify-center')}>
|
||||
<span>Not shared with any users.</span>
|
||||
</div>
|
||||
) : null}
|
||||
{permissions && permissions.length > 0 ? (
|
||||
<>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>User</Th>
|
||||
<Th>Permission</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{permissions.map((p) => (
|
||||
<Tr key={p.id}>
|
||||
<Td className={cx('p-1')}>
|
||||
<div
|
||||
className={cx(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'gap-1',
|
||||
)}
|
||||
>
|
||||
<Avatar
|
||||
name={p.user.fullName}
|
||||
src={p.user.picture}
|
||||
size="sm"
|
||||
className={cx('w-[40px]', 'h-[40px]')}
|
||||
/>
|
||||
<div className={cx('flex', 'flex-col', 'gap-0.5')}>
|
||||
<Text noOfLines={1}>{p.user.fullName}</Text>
|
||||
<span className={cx('text-gray-500')}>
|
||||
{p.user.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge>{p.permission}</Badge>
|
||||
</Td>
|
||||
<Td className={cx('text-end')}>
|
||||
<IconButton
|
||||
icon={<IconDelete />}
|
||||
colorScheme="red"
|
||||
aria-label=""
|
||||
isLoading={permissionBeingRevoked === p.id}
|
||||
isDisabled={user?.id === p.user.id}
|
||||
onClick={() => handleRevokeUserPermission(p)}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SharingUsers
|
Reference in New Issue
Block a user