ajout app

This commit is contained in:
2024-04-17 20:22:30 +02:00
parent cc017cfc5e
commit f9d05a2fd3
8025 changed files with 729805 additions and 0 deletions

View 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

View File

@ -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

View 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

View 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