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,165 @@
import { useEffect } from 'react'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import cx from 'classnames'
import { Helmet } from 'react-helmet-async'
import FileAPI from '@/client/api/file'
import WorkspaceAPI from '@/client/api/workspace'
import { swrConfig } from '@/client/options'
import Path from '@/components/common/path'
import FileCopy from '@/components/file/file-copy'
import FileCreate from '@/components/file/file-create'
import FileMove from '@/components/file/file-move'
import FileRename from '@/components/file/file-rename'
import FileToolbar from '@/components/file/file-toolbar'
import FileDelete from '@/components/file/fle-idelete'
import FileList from '@/components/file/list'
import FileSharing from '@/components/file/sharing'
import { decodeQuery } from '@/helpers/query'
import { filePaginationSteps, filesPaginationStorage } from '@/infra/pagination'
import {
PagePagination,
Spinner,
usePageMonitor,
usePagePagination,
variables,
} from '@/lib'
import { listUpdated } from '@/store/entities/files'
import { useAppDispatch, useAppSelector } from '@/store/hook'
import { selectionUpdated } from '@/store/ui/files'
const WorkspaceFilesPage = () => {
const navigate = useNavigate()
const { id, fileId } = useParams()
const [searchParams] = useSearchParams()
const query = decodeQuery(searchParams.get('q') as string)
const dispatch = useAppDispatch()
const sortBy = useAppSelector((state) => state.ui.files.sortBy)
const sortOrder = useAppSelector((state) => state.ui.files.sortOrder)
const iconScale = useAppSelector((state) => state.ui.files.iconScale)
const { data: workspace } = WorkspaceAPI.useGetById(id, swrConfig())
const { page, size, steps, setPage, setSize } = usePagePagination({
navigate,
location,
storage: filesPaginationStorage(),
steps: filePaginationSteps(),
})
const {
data: list,
error,
isLoading,
} = FileAPI.useList(
fileId!,
{
size,
page,
sortBy,
sortOrder,
query: query ? { text: query } : undefined,
},
swrConfig(),
)
const { hasPageSwitcher, hasSizeSelector } = usePageMonitor({
totalElements: list?.totalElements || 0,
totalPages: list?.totalPages || 1,
steps,
})
const hasPagination = hasPageSwitcher || hasSizeSelector
useEffect(() => {
if (list) {
dispatch(listUpdated(list))
}
}, [list, dispatch])
return (
<>
<Helmet>{workspace && <title>{workspace.name}</title>}</Helmet>
<div
className={cx(
'flex',
'flex-col',
'w-full',
'gap-2.5',
'grow',
'overflow-hidden',
)}
>
{workspace && fileId ? (
<Path
rootId={workspace.rootId}
fileId={fileId}
maxCharacters={30}
onClick={(fileId) => {
dispatch(selectionUpdated([]))
navigate(`/workspace/${workspace.id}/file/${fileId}`)
}}
/>
) : null}
<FileToolbar list={list} />
<div
className={cx(
'flex',
'flex-col',
'gap-1.5',
'grow',
'overflow-y-auto',
'overflow-x-hidden',
)}
>
<div
className={cx(
'w-full',
'overflow-y-auto',
'overflow-x-hidden',
'border-t',
'border-t-gray-300',
'dark:border-t-gray-600',
{
'border-b': hasPagination,
'border-b-gray-300': hasPagination,
'dark:border-b-gray-600': hasPagination,
},
'pt-1.5',
'flex-grow',
)}
onClick={() => dispatch(selectionUpdated([]))}
>
{isLoading ? (
<div
className={cx(
'flex',
'items-center',
'justify-center',
'h-full',
)}
>
<Spinner />
</div>
) : null}
{list && !error ? <FileList list={list} scale={iconScale} /> : null}
</div>
{list ? (
<PagePagination
style={{ alignSelf: 'end', paddingBottom: variables.spacing }}
totalElements={list.totalElements}
totalPages={list.totalPages}
page={page}
size={size}
steps={steps}
setPage={setPage}
setSize={setSize}
/>
) : null}
</div>
</div>
{list ? <FileSharing list={list} /> : null}
<FileMove />
<FileCopy />
<FileCreate />
<FileDelete />
<FileRename />
</>
)
}
export default WorkspaceFilesPage

View File

@@ -0,0 +1,51 @@
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 WorkspaceAPI from '@/client/api/workspace'
import { swrConfig } from '@/client/options'
const WorkspaceLayout = () => {
const location = useLocation()
const { id } = useParams()
const navigate = useNavigate()
const { data: workspace } = WorkspaceAPI.useGetById(id, swrConfig())
const [tabIndex, setTabIndex] = useState(0)
useEffect(() => {
const segments = location.pathname.split('/')
const segment = segments[segments.length - 1]
if (segment === 'settings') {
setTabIndex(1)
} else {
setTabIndex(0)
}
}, [location])
if (!workspace) {
return null
}
return (
<div className={cx('flex', 'flex-col', 'gap-3.5', 'h-full')}>
<Heading className={cx('text-heading')}>{workspace.name}</Heading>
<Tabs variant="solid-rounded" colorScheme="gray" index={tabIndex}>
<TabList>
<Tab
onClick={() =>
navigate(`/workspace/${id}/file/${workspace.rootId}`)
}
>
Files
</Tab>
<Tab onClick={() => navigate(`/workspace/${id}/settings`)}>
Settings
</Tab>
</TabList>
</Tabs>
<Outlet />
</div>
)
}
export default WorkspaceLayout

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 WorkspaceAPI, { SortOrder } from '@/client/api/workspace'
import { swrConfig } from '@/client/options'
import { CreateWorkspaceButton } from '@/components/top-bar/top-bar-buttons'
import prettyDate from '@/helpers/pretty-date'
import { decodeQuery } from '@/helpers/query'
import { workspacePaginationStorage } from '@/infra/pagination'
import { SectionSpinner, PagePagination, usePagePagination } from '@/lib'
const WorkspaceListPage = () => {
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: workspacePaginationStorage(),
})
const {
data: list,
error,
mutate,
} = WorkspaceAPI.useList(
{ query, page, size, sortOrder: SortOrder.Desc },
swrConfig(),
)
useEffect(() => {
mutate()
}, [query, page, size, mutate])
return (
<>
<Helmet>
<title>Workspaces</title>
</Helmet>
<div className={cx('flex', 'flex-col', 'gap-3.5', 'pb-3.5')}>
<Heading className={cx('pl-2', 'text-heading')}>Workspaces</Heading>
{!list && error && (
<div
className={cx(
'flex',
'items-center',
'justify-center',
'h-[300px]',
)}
>
<span>Failed to load workspaces.</span>
</div>
)}
{!list && !error && <SectionSpinner />}
{list && list.data.length === 0 && !error ? (
<div
className={cx(
'flex',
'items-center',
'justify-center',
'h-[300px]',
)}
>
<div className={cx('flex', 'flex-col', 'gap-1.5', 'items-center')}>
<span>There are no workspaces.</span>
<CreateWorkspaceButton />
</div>
</div>
) : null}
{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((w) => (
<Tr key={w.id}>
<Td>
<div
className={cx(
'flex',
'flex-row',
'gap-1.5',
'items-center',
)}
>
<Avatar
name={w.name}
size="sm"
className={cx('w-[40px]', 'h-[40px]')}
/>
<ChakraLink
as={Link}
to={`/workspace/${w.id}/file/${w.rootId}`}
className={cx('no-underline')}
>
<span>{w.name}</span>
</ChakraLink>
</div>
</Td>
<Td>
<ChakraLink
as={Link}
to={`/organization/${w.organization.id}/member`}
className={cx('no-underline')}
>
{w.organization.name}
</ChakraLink>
</Td>
<Td>
<Badge>{w.permission}</Badge>
</Td>
<Td>{prettyDate(w.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 WorkspaceListPage

View File

@@ -0,0 +1,147 @@
import { useMemo, useState } from 'react'
import { useParams } from 'react-router-dom'
import {
Divider,
IconButton,
IconButtonProps,
Progress,
} from '@chakra-ui/react'
import cx from 'classnames'
import { Helmet } from 'react-helmet-async'
import { geEditorPermission } from '@/client/api/permission'
import StorageAPI from '@/client/api/storage'
import WorkspaceAPI from '@/client/api/workspace'
import { swrConfig } from '@/client/options'
import WorkspaceDelete from '@/components/workspace/workspace-delete'
import WorkspaceEditName from '@/components/workspace/workspace-edit-name'
import WorkspaceEditStorageCapacity from '@/components/workspace/workspace-edit-storage-capacity'
import prettyBytes from '@/helpers/pretty-bytes'
import { IconEdit, IconDelete, SectionSpinner } from '@/lib'
const EditButton = (props: IconButtonProps) => (
<IconButton icon={<IconEdit />} {...props} />
)
const Spacer = () => <div className={cx('grow')} />
const WorkspaceSettingsPage = () => {
const { id } = useParams()
const { data: workspace, error: workspaceError } = WorkspaceAPI.useGetById(
id,
swrConfig(),
)
const { data: storageUsage, error: storageUsageError } =
StorageAPI.useGetWorkspaceUsage(id, swrConfig())
const hasEditPermission = useMemo(
() => workspace && geEditorPermission(workspace.permission),
[workspace],
)
const [isNameModalOpen, setIsNameModalOpen] = useState(false)
const [isStorageCapacityModalOpen, setIsStorageCapacityModalOpen] =
useState(false)
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const sectionClassName = cx('flex', 'flex-col', 'gap-1', 'py-1.5')
const rowClassName = cx(
'flex',
'flex-row',
'items-center',
'gap-1',
`h-[40px]`,
)
if (workspaceError) {
return null
}
if (!workspace) {
return <SectionSpinner />
}
return (
<>
<Helmet>
<title>{workspace.name}</title>
</Helmet>
<div className={cx('flex', 'flex-col', 'gap-0')}>
<div className={sectionClassName}>
<span className={cx('font-bold')}>Storage</span>
{storageUsageError && <span>Failed to load storage usage.</span>}
{storageUsage && !storageUsageError && (
<>
<span>
{prettyBytes(storageUsage.bytes)} of{' '}
{prettyBytes(storageUsage.maxBytes)} used
</span>
<Progress value={storageUsage.percentage} hasStripe />
</>
)}
{!storageUsage && !storageUsageError && (
<>
<span>Calculating</span>
<Progress value={0} hasStripe />
</>
)}
<Divider />
<div className={rowClassName}>
<span>Storage capacity</span>
<Spacer />
<span>{prettyBytes(workspace.storageCapacity)}</span>
<EditButton
aria-label=""
isDisabled={!hasEditPermission}
onClick={() => {
setIsStorageCapacityModalOpen(true)
}}
/>
</div>
<Divider className={cx('mb-1.5')} />
<span className={cx('font-bold')}>Basics</span>
<div className={rowClassName}>
<span>Name</span>
<Spacer />
<span>{workspace.name}</span>
<EditButton
aria-label=""
isDisabled={!hasEditPermission}
onClick={() => {
setIsNameModalOpen(true)
}}
/>
</div>
</div>
<div className={sectionClassName}>
<span className={cx('font-bold')}>Advanced</span>
<div className={rowClassName}>
<span>Delete permanently</span>
<Spacer />
<IconButton
icon={<IconDelete />}
variant="solid"
colorScheme="red"
isDisabled={!hasEditPermission}
aria-label=""
onClick={() => setIsDeleteModalOpen(true)}
/>
</div>
</div>
</div>
<WorkspaceEditName
open={isNameModalOpen}
workspace={workspace}
onClose={() => setIsNameModalOpen(false)}
/>
<WorkspaceEditStorageCapacity
open={isStorageCapacityModalOpen}
workspace={workspace}
onClose={() => setIsStorageCapacityModalOpen(false)}
/>
<WorkspaceDelete
open={isDeleteModalOpen}
workspace={workspace}
onClose={() => setIsDeleteModalOpen(false)}
/>
</>
)
}
export default WorkspaceSettingsPage