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,43 @@
import { useEffect, useState } from 'react'
import { Outlet, useLocation, useNavigate, useParams } from 'react-router-dom'
import { Heading, Tab, TabList, Tabs } from '@chakra-ui/react'
import cx from 'classnames'
import GroupAPI from '@/client/api/group'
import { swrConfig } from '@/client/options'
const GroupLayout = () => {
const location = useLocation()
const navigate = useNavigate()
const { id } = useParams()
const { data: group } = GroupAPI.useGetById(id, swrConfig())
const [tabIndex, setTabIndex] = useState(0)
useEffect(() => {
const segments = location.pathname.split('/')
const segment = segments[segments.length - 1]
if (segment === 'member') {
setTabIndex(0)
} else if (segment === 'settings') {
setTabIndex(1)
}
}, [location])
if (!group) {
return null
}
return (
<div className={cx('flex', 'flex-col', 'gap-3.5')}>
<Heading className={cx('text-heading')}>{group.name}</Heading>
<Tabs variant="solid-rounded" colorScheme="gray" index={tabIndex}>
<TabList>
<Tab onClick={() => navigate(`/group/${id}/member`)}>Members</Tab>
<Tab onClick={() => navigate(`/group/${id}/settings`)}>Settings</Tab>
</TabList>
</Tabs>
<Outlet />
</div>
)
}
export default GroupLayout

View File

@ -0,0 +1,159 @@
import { useEffect } from 'react'
import {
Link,
useLocation,
useNavigate,
useSearchParams,
} from 'react-router-dom'
import {
Heading,
Link as ChakraLink,
Table,
Tbody,
Td,
Th,
Thead,
Tr,
Avatar,
Badge,
} from '@chakra-ui/react'
import cx from 'classnames'
import { Helmet } from 'react-helmet-async'
import GroupAPI, { SortOrder } from '@/client/api/group'
import { swrConfig } from '@/client/options'
import { CreateGroupButton } from '@/components/top-bar/top-bar-buttons'
import prettyDate from '@/helpers/pretty-date'
import { decodeQuery } from '@/helpers/query'
import { groupPaginationStorage } from '@/infra/pagination'
import { SectionSpinner, PagePagination, usePagePagination } from '@/lib'
const GroupListPage = () => {
const navigate = useNavigate()
const location = useLocation()
const [searchParams] = useSearchParams()
const query = decodeQuery(searchParams.get('q') as string)
const { page, size, steps, setPage, setSize } = usePagePagination({
navigate,
location,
storage: groupPaginationStorage(),
})
const {
data: list,
error,
mutate,
} = GroupAPI.useList(
{ query, page, size, sortOrder: SortOrder.Desc },
swrConfig(),
)
useEffect(() => {
mutate()
}, [query, page, size, mutate])
return (
<>
<Helmet>
<title>Groups</title>
</Helmet>
<div className={cx('flex', 'flex-col', 'gap-3.5', 'pb-3.5')}>
<Heading className={cx('pl-2', 'text-heading')}>Groups</Heading>
{error && (
<div
className={cx(
'flex',
'items-center',
'justify-center',
'h-[300px]',
)}
>
<span>Failed to load groups.</span>
</div>
)}
{!list && !error && <SectionSpinner />}
{list && list.data.length === 0 && (
<div
className={cx(
'flex',
'items-center',
'justify-center',
'h-[300px]',
)}
>
<div className={cx('flex', 'flex-col', 'gap-1.5')}>
<span>There are no groups.</span>
<CreateGroupButton />
</div>
</div>
)}
{list && list.data.length > 0 && (
<Table variant="simple">
<Thead>
<Tr>
<Th>Name</Th>
<Th>Organization</Th>
<Th>Permission</Th>
<Th>Date</Th>
</Tr>
</Thead>
<Tbody>
{list.data.map((g) => (
<Tr key={g.id}>
<Td>
<div
className={cx(
'flex',
'flex-row',
'items-center',
'gap-1.5',
)}
>
<Avatar
name={g.name}
size="sm"
className={cx('w-[40px]', 'h-[40px]')}
/>
<ChakraLink
as={Link}
to={`/group/${g.id}/member`}
className={cx('no-underline')}
>
{g.name}
</ChakraLink>
</div>
</Td>
<Td>
<ChakraLink
as={Link}
to={`/organization/${g.organization.id}/member`}
className={cx('no-underline')}
>
{g.organization.name}
</ChakraLink>
</Td>
<Td>
<Badge>{g.permission}</Badge>
</Td>
<Td>{prettyDate(g.createTime)}</Td>
</Tr>
))}
</Tbody>
</Table>
)}
{list && (
<PagePagination
style={{ alignSelf: 'end' }}
totalElements={list.totalElements}
totalPages={list.totalPages}
page={page}
size={size}
steps={steps}
setPage={setPage}
setSize={setSize}
/>
)}
</div>
</>
)
}
export default GroupListPage

View File

@ -0,0 +1,209 @@
import { useState } from 'react'
import {
useLocation,
useNavigate,
useParams,
useSearchParams,
} from 'react-router-dom'
import {
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
Table,
Tbody,
Td,
Th,
Thead,
Tr,
Button,
Avatar,
Portal,
} from '@chakra-ui/react'
import cx from 'classnames'
import { Helmet } from 'react-helmet-async'
import GroupAPI from '@/client/api/group'
import { geEditorPermission } from '@/client/api/permission'
import UserAPI, { SortBy, SortOrder } from '@/client/api/user'
import { User as IdPUser } from '@/client/idp/user'
import { swrConfig } from '@/client/options'
import GroupAddMember from '@/components/group/group-add-member'
import GroupRemoveMember from '@/components/group/group-remove-member'
import { decodeQuery } from '@/helpers/query'
import { groupMemberPaginationStorage } from '@/infra/pagination'
import {
IconLogout,
IconPersonAdd,
SectionSpinner,
PagePagination,
usePagePagination,
IconMoreVert,
} from '@/lib'
const GroupMembersPage = () => {
const navigate = useNavigate()
const location = useLocation()
const { id } = useParams()
const { data: group, error: groupError } = GroupAPI.useGetById(
id,
swrConfig(),
)
const { page, size, steps, setPage, setSize } = usePagePagination({
navigate,
location,
storage: groupMemberPaginationStorage(),
})
const [searchParams] = useSearchParams()
const query = decodeQuery(searchParams.get('q') as string)
const {
data: list,
error: membersError,
mutate,
} = UserAPI.useList(
{
query,
groupId: id,
page,
size,
sortBy: SortBy.FullName,
sortOrder: SortOrder.Asc,
},
swrConfig(),
)
const [userToRemove, setUserToRemove] = useState<IdPUser>()
const [isAddMembersModalOpen, setIsAddMembersModalOpen] = useState(false)
const [isRemoveMemberModalOpen, setIsRemoveMemberModalOpen] =
useState<boolean>(false)
if (groupError || membersError) {
return null
}
if (!group || !list) {
return <SectionSpinner />
}
return (
<>
<Helmet>
<title>{group.name}</title>
</Helmet>
{list.data.length > 0 && (
<div className={cx('flex', 'flex-col', 'gap-3.5', 'pb-3.5')}>
<Table variant="simple">
<Thead>
<Tr>
<Th>Full name</Th>
<Th>Email</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{list.data.map((u) => (
<Tr key={u.id}>
<Td>
<div
className={cx(
'flex',
'flex-row',
'gap-1.5',
'items-center',
)}
>
<Avatar name={u.fullName} src={u.picture} />
<span>{u.fullName}</span>
</div>
</Td>
<Td>{u.email}</Td>
<Td className={cx('text-right')}>
<Menu>
<MenuButton
as={IconButton}
icon={<IconMoreVert />}
variant="ghost"
aria-label=""
/>
<Portal>
<MenuList>
<MenuItem
icon={<IconLogout />}
className={cx('text-red-500')}
isDisabled={!geEditorPermission(group.permission)}
onClick={() => {
setUserToRemove(u)
setIsRemoveMemberModalOpen(true)
}}
>
Remove From Group
</MenuItem>
</MenuList>
</Portal>
</Menu>
</Td>
</Tr>
))}
</Tbody>
</Table>
{list && (
<PagePagination
style={{ alignSelf: 'end' }}
totalElements={list.totalElements}
totalPages={list.totalPages}
page={page}
size={size}
steps={steps}
setPage={setPage}
setSize={setSize}
/>
)}
{userToRemove && (
<GroupRemoveMember
isOpen={isRemoveMemberModalOpen}
user={userToRemove}
group={group}
onCompleted={() => mutate()}
onClose={() => setIsRemoveMemberModalOpen(false)}
/>
)}
</div>
)}
{list.data.length === 0 && (
<>
<Helmet>
<title>{group.name}</title>
</Helmet>
<div
className={cx(
'flex',
'items-center',
'justify-center',
'h-[300px]',
)}
>
<div className={cx('flex', 'flex-col', 'gap-1.5', 'items-center')}>
<span>This group has no members.</span>
{geEditorPermission(group.permission) && (
<Button
leftIcon={<IconPersonAdd />}
onClick={() => {
setIsAddMembersModalOpen(true)
}}
>
Add Members
</Button>
)}
</div>
</div>
<GroupAddMember
open={isAddMembersModalOpen}
group={group}
onClose={() => setIsAddMembersModalOpen(false)}
/>
</>
)}
</>
)
}
export default GroupMembersPage

View File

@ -0,0 +1,111 @@
import { useMemo, useState } from 'react'
import { useParams } from 'react-router-dom'
import { Divider, IconButton } from '@chakra-ui/react'
import cx from 'classnames'
import { Helmet } from 'react-helmet-async'
import GroupAPI from '@/client/api/group'
import { geEditorPermission, geOwnerPermission } from '@/client/api/permission'
import { swrConfig } from '@/client/options'
import GroupAddMember from '@/components/group/group-add-member'
import GroupDelete from '@/components/group/group-delete'
import GroupEditName from '@/components/group/group-edit-name'
import { IconEdit, IconDelete, IconPersonAdd, SectionSpinner } from '@/lib'
const Spacer = () => <div className={cx('grow')} />
const GroupSettingsPage = () => {
const { id } = useParams()
const { data: group, error } = GroupAPI.useGetById(id, swrConfig())
const [isNameModalOpen, setIsNameModalOpen] = useState(false)
const [isAddMembersModalOpen, setIsAddMembersModalOpen] = useState(false)
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
const hasEditPermission = useMemo(
() => group && geEditorPermission(group.permission),
[group],
)
const hasOwnerPermission = useMemo(
() => group && geOwnerPermission(group.permission),
[group],
)
const sectionClassName = cx('flex', 'flex-col', 'gap-1', 'py-1.5')
const rowClassName = cx(
'flex',
'flex-row',
'items-center',
'gap-1',
`h-[40px]`,
)
if (error) {
return null
}
if (!group) {
return <SectionSpinner />
}
return (
<>
<Helmet>
<title>{group.name}</title>
</Helmet>
<div className={sectionClassName}>
<div className={rowClassName}>
<span>Name</span>
<Spacer />
<span>{group.name}</span>
<IconButton
icon={<IconEdit />}
isDisabled={!hasEditPermission}
aria-label=""
onClick={() => {
setIsNameModalOpen(true)
}}
/>
</div>
<Divider />
<div className={rowClassName}>
<span>Add members</span>
<Spacer />
<IconButton
icon={<IconPersonAdd />}
isDisabled={!hasOwnerPermission}
aria-label=""
onClick={() => {
setIsAddMembersModalOpen(true)
}}
/>
</div>
<Divider />
<div className={rowClassName}>
<span>Delete permanently</span>
<Spacer />
<IconButton
icon={<IconDelete />}
variant="solid"
colorScheme="red"
isDisabled={!hasOwnerPermission}
aria-label=""
onClick={() => setDeleteModalOpen(true)}
/>
</div>
<GroupEditName
open={isNameModalOpen}
group={group}
onClose={() => setIsNameModalOpen(false)}
/>
<GroupAddMember
open={isAddMembersModalOpen}
group={group}
onClose={() => setIsAddMembersModalOpen(false)}
/>
<GroupDelete
open={deleteModalOpen}
group={group}
onClose={() => setDeleteModalOpen(false)}
/>
</div>
</>
)
}
export default GroupSettingsPage