|
import { |
|
BranchesOutlined, GlobalOutlined, |
|
HomeOutlined, |
|
ProfileOutlined, |
|
TeamOutlined |
|
} from '@ant-design/icons' |
|
import { Button, Col, Descriptions, Input, Layout, Menu, Modal, Row, Table, TablePaginationConfig, Typography } from 'antd' |
|
import { FilterValue, SorterResult, TableCurrentDataSource } from 'antd/lib/table/interface' |
|
import moment from 'moment' |
|
import prettyBytes from 'pretty-bytes' |
|
import QueryString from 'qs' |
|
import React, { useEffect, useState } from 'react' |
|
import { useThemeSwitcher } from 'react-css-theme-switcher' |
|
import { useHistory } from 'react-router-dom' |
|
import useSWR from 'swr' |
|
import { fetcher } from '../../../utils/Fetcher' |
|
import Footer from '../../components/Footer' |
|
import Navbar from '../../components/Navbar' |
|
import Breadcrumb from '../../dashboard/components/Breadcrumb' |
|
import Icon from './Icon' |
|
|
|
interface Props { |
|
me: any, |
|
data: any |
|
} |
|
|
|
const TableFiles: React.FC<Props> = ({ me, data }) => { |
|
const history = useHistory() |
|
const [dataChanges, setDataChanges] = useState<{ |
|
pagination?: TablePaginationConfig, |
|
filters?: Record<string, FilterValue | null>, |
|
sorter?: SorterResult<any> | SorterResult<any>[] |
|
}>() |
|
const { currentTheme } = useThemeSwitcher() |
|
const [parent, setParent] = useState<Record<string, any> | null>() |
|
const [breadcrumbs, setBreadcrumbs] = useState<any[]>([data?.file || { id: null, name: <><HomeOutlined /></> }]) |
|
const [scrollTop, setScrollTop] = useState<number>(0) |
|
const [keyword, setKeyword] = useState<string>() |
|
const [filesData, setFilesData] = useState<any>() |
|
const [params, setParams] = useState<any>() |
|
const [loading, setLoading] = useState<boolean>(false) |
|
const [showDetails, setShowDetails] = useState<any>() |
|
const { data: filesParts } = useSWR(showDetails ? `/files?name.like=${showDetails.name.replace(/\.part0*\d+$/, '')}&user_id=${showDetails.user_id}${me?.user.id !== showDetails.user_id ? '&shared=1' : ''}&parent_id${showDetails.parent_id ? `=${showDetails.parent_id}` : '=null'}` : null, fetcher) |
|
const [popup, setPopup] = useState<{ visible: boolean, x?: number, y?: number, row?: any }>() |
|
const { data: files, mutate: _refetch } = useSWR(data?.file.type === 'folder' && data?.file.sharing_options?.includes('*') && params ? `/files?exclude_parts=1&${QueryString.stringify(params)}` : null, fetcher, { onSuccess: files => { |
|
setLoading(false) |
|
if (files?.files) { |
|
let newData: any[] = [] |
|
if (!params?.offset || !dataChanges?.pagination?.current || dataChanges?.pagination?.current === 1) { |
|
newData = files.files.map((file: any) => ({ ...file, key: file.id })) |
|
} else { |
|
newData = [ |
|
...filesData.map(row => files.files.find((file: any) => file.id === row.id) || row).map(file => ({ ...file, key: file.id })), |
|
...files.files.map((file: any) => ({ ...file, key: file.id })) |
|
].reduce((res, row) => [ |
|
...res, !res.filter(Boolean).find((r: any) => r.id === row.id) ? row : null |
|
], []).filter(Boolean) |
|
} |
|
|
|
setFilesData(newData) |
|
} |
|
} }) |
|
|
|
const PAGE_SIZE = 10 |
|
|
|
const fetch = (pagination?: TablePaginationConfig, filters?: Record<string, FilterValue | null>, sorter?: SorterResult<any> | SorterResult<any>[], actions?: TableCurrentDataSource<any>) => { |
|
setLoading(true) |
|
setParams({ |
|
|
|
parent_id: parent?.link_id || parent?.id || data?.file.id, |
|
...keyword ? { 'name.ilike': `%${keyword}%` } : {}, |
|
shared: 1, |
|
limit: PAGE_SIZE, |
|
offset: pagination?.current === 1 || actions?.action || keyword && params?.offset ? 0 : filesData?.length, |
|
...Object.keys(filters || {})?.reduce((res, key: string) => { |
|
if (!filters) return res |
|
if (key === 'type' && filters[key]?.length) { |
|
return { ...res, [`${key}.in`]: `(${filters[key]?.map(val => `'${val}'`).join(',')})` } |
|
} |
|
return { ...res, [key]: filters[key]?.[0] } |
|
}, {}), |
|
...(sorter as SorterResult<any>)?.order ? { |
|
sort: `${(sorter as SorterResult<any>).column?.dataIndex}:${(sorter as SorterResult<any>).order?.replace(/end$/gi, '')}` |
|
} : { sort: 'created_at:desc' }, |
|
t: new Date().getTime() |
|
}) |
|
} |
|
|
|
const onChange = async (pagination?: TablePaginationConfig, filters?: Record<string, FilterValue | null>, sorter?: SorterResult<any> | SorterResult<any>[], actions?: TableCurrentDataSource<any>) => { |
|
setDataChanges({ pagination, filters, sorter }) |
|
fetch(pagination, filters, sorter, actions) |
|
} |
|
|
|
const onRowClick = (row: any) => { |
|
if (row.type === 'folder') { |
|
setParent(row) |
|
setBreadcrumbs([...breadcrumbs, row]) |
|
|
|
const searchParams = new URLSearchParams(window.location.search) |
|
searchParams.set('parent', row.id) |
|
return history.push(`${location.pathname}?${searchParams.toString()}`) |
|
} else { |
|
return history.push(`/view/${row.id}`) |
|
} |
|
} |
|
|
|
useEffect(() => { |
|
const nextPage = () => { |
|
setScrollTop(document.body.scrollTop) |
|
} |
|
nextPage() |
|
document.body.addEventListener('scroll', nextPage) |
|
}, []) |
|
|
|
useEffect(() => { |
|
const footer = document.querySelector('.ant-layout-footer') |
|
if (scrollTop >= document.body.scrollHeight - document.body.clientHeight - (footer?.clientHeight || 0) && files?.files.length >= PAGE_SIZE) { |
|
onChange({ ...dataChanges?.pagination, current: (dataChanges?.pagination?.current || 1) + 1 }, dataChanges?.filters, dataChanges?.sorter) |
|
} |
|
}, [scrollTop]) |
|
|
|
useEffect(() => { |
|
if (parent !== undefined || keyword !== undefined) { |
|
onChange({ ...dataChanges?.pagination, current: 1 }, dataChanges?.filters, dataChanges?.sorter) |
|
setScrollTop(0) |
|
} |
|
}, [keyword, parent]) |
|
|
|
const ContextMenu = () => { |
|
const baseProps = { |
|
style: { margin: 0 } |
|
} |
|
if (!popup?.visible) return <></> |
|
if (popup?.row) { |
|
return <Menu style={{ zIndex: 1, position: 'absolute', left: `${popup?.x}px`, top: `${popup?.y}px`, boxShadow: '0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05)' }}> |
|
<Menu.Item {...baseProps} |
|
icon={<ProfileOutlined />} |
|
key="details" |
|
onClick={() => setShowDetails(popup?.row)}>Details</Menu.Item> |
|
</Menu> |
|
} |
|
return <></> |
|
} |
|
|
|
return <Layout> |
|
<Layout.Content> |
|
<Navbar user={me?.user} /> |
|
<Row style={{ minHeight: '80vh', marginBottom: '100px', padding: '20px 12px 0' }}> |
|
<Col lg={{ span: 18, offset: 3 }} md={{ span: 20, offset: 2 }} span={24}> |
|
<Typography.Paragraph style={{ float: 'left' }}> |
|
<Breadcrumb dataSource={[breadcrumbs, setBreadcrumbs]} dataParent={[parent, setParent]} /> |
|
</Typography.Paragraph> |
|
<Typography.Paragraph style={{ textAlign: 'right' }}> |
|
<Input.Search style={{ width: '210px' }} className="input-search-round" placeholder="Search..." enterButton onSearch={setKeyword} allowClear /> |
|
</Typography.Paragraph> |
|
<Table |
|
className="tableFiles" |
|
loading={!data || loading} |
|
showSorterTooltip={false} |
|
dataSource={filesData} |
|
columns={[ |
|
{ |
|
title: 'File', |
|
dataIndex: 'name', |
|
key: 'type', |
|
sorter: true, |
|
sortOrder: (dataChanges?.sorter as SorterResult<any>)?.column?.dataIndex === 'name' ? (dataChanges?.sorter as SorterResult<any>).order : undefined, |
|
filters: [ |
|
{ |
|
text: 'Folder', |
|
value: 'folder' |
|
}, |
|
{ |
|
text: 'Image', |
|
value: 'image' |
|
}, |
|
{ |
|
text: 'Video', |
|
value: 'video' |
|
}, |
|
{ |
|
text: 'Audio', |
|
value: 'audio' |
|
}, |
|
{ |
|
text: 'Document', |
|
value: 'document' |
|
}, |
|
{ |
|
text: 'Unknown', |
|
value: 'unknown' |
|
} |
|
], |
|
ellipsis: true, |
|
onCell: (row: any) => ({ |
|
onClick: () => onRowClick(row) |
|
}), |
|
render: (_: any, row: any) => { |
|
let type: any |
|
if (row.sharing_options?.includes('*')) { |
|
type = <GlobalOutlined /> |
|
} else if (row.sharing_options?.length) { |
|
type = <TeamOutlined /> |
|
} |
|
|
|
return <Button type="link" block style={{ textAlign: 'left', padding: 0, color: currentTheme === 'dark' ? '#FFFFFFD9' : '#000000D9' }}> |
|
{row.link_id ? <BranchesOutlined /> : '' } {type} <Icon type={row.type} /> {row.name.replace(/\.part0*\d+$/, '')} |
|
</Button> |
|
} |
|
}, |
|
{ |
|
title: 'Size', |
|
dataIndex: 'size', |
|
key: 'size', |
|
sorter: true, |
|
sortOrder: (dataChanges?.sorter as SorterResult<any>)?.column?.key === 'size' ? (dataChanges?.sorter as SorterResult<any>).order : undefined, |
|
responsive: ['md'], |
|
width: 100, |
|
align: 'center', |
|
render: (value: any) => { |
|
if (Number(value) === 2_000_000_000) { |
|
return '> 2 GB' |
|
} |
|
return value ? prettyBytes(Number(value)) : '-' |
|
} |
|
}, |
|
{ |
|
title: 'Uploaded At', |
|
dataIndex: 'uploaded_at', |
|
key: 'uploaded_at', |
|
sorter: true, |
|
sortOrder: (dataChanges?.sorter as SorterResult<any>)?.column?.key === 'uploaded_at' ? (dataChanges?.sorter as SorterResult<any>).order : undefined, |
|
responsive: ['md'], |
|
width: 250, |
|
align: 'center', |
|
render: (value: any, row: any) => row.upload_progress !== null ? <>Uploading {Number((row.upload_progress * 100).toFixed(2))}%</> : moment(value).local().format('llll') |
|
} |
|
]} |
|
onChange={onChange} |
|
pagination={false} |
|
scroll={{ x: 330 }} |
|
onRow={(row, index) => ({ |
|
index, |
|
onContextMenu: e => { |
|
// if (tab !== 'mine') return |
|
|
|
e.preventDefault() |
|
if (!popup?.visible) { |
|
document.addEventListener('click', function onClickOutside() { |
|
setPopup({ visible: false }) |
|
document.removeEventListener('click', onClickOutside) |
|
}) |
|
} |
|
// const parent = document.querySelector('.ant-col-24.ant-col-md-20.ant-col-md-offset-2') |
|
setPopup({ |
|
row, |
|
visible: true, |
|
x: e.clientX, |
|
y: e.clientY |
|
// x: e.clientX - (parent?.getBoundingClientRect().left || 0), |
|
// y: e.clientY - (parent?.getBoundingClientRect().top || 0) |
|
}) |
|
} |
|
})} /> |
|
</Col> |
|
</Row> |
|
</Layout.Content> |
|
<Footer me={me} /> |
|
<ContextMenu /> |
|
<Modal title={<Typography.Text ellipsis><Icon type={showDetails?.type} /> {showDetails?.name.replace(/\.part0*\d+$/, '')}</Typography.Text>} |
|
visible={Boolean(showDetails)} |
|
onCancel={() => setShowDetails(undefined)} |
|
okText="View" |
|
onOk={() => onRowClick(showDetails)} |
|
cancelButtonProps={{ shape: 'round' }} |
|
okButtonProps={{ shape: 'round' }}> |
|
<Descriptions column={1}> |
|
<Descriptions.Item label="Size"> |
|
{filesParts?.length ? prettyBytes(filesParts?.files.reduce((res: number, file: any) => res + Number(file.size), 0)) + ` (${filesParts?.length} parts)` : showDetails?.size && prettyBytes(Number(showDetails?.size || 0))} |
|
</Descriptions.Item> |
|
<Descriptions.Item label="Uploaded At">{moment(showDetails?.uploaded_at).local().format('lll')}</Descriptions.Item> |
|
</Descriptions> |
|
</Modal> |
|
</Layout> |
|
} |
|
|
|
export default TableFiles |