mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-08 10:22:46 +00:00
49
frontend/src/components/common/fixed-width-table.js
Normal file
49
frontend/src/components/common/fixed-width-table.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const FixedWidthTable = ({ className, headers, theadOptions = {}, children }) => {
|
||||
const [containerWidth, setContainerWidth] = useState(0);
|
||||
const fixedWidth = useMemo(() => headers.reduce((pre, cur) => cur.isFixed ? cur.width + pre : pre, 0), [headers]);
|
||||
|
||||
const containerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const handleResize = () => {
|
||||
if (!container) return;
|
||||
setContainerWidth(container.offsetWidth);
|
||||
};
|
||||
const resizeObserver = new ResizeObserver(handleResize);
|
||||
container && resizeObserver.observe(container);
|
||||
|
||||
return () => {
|
||||
container && resizeObserver.unobserve(container);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<table ref={containerRef} className={className}>
|
||||
<thead { ...theadOptions }>
|
||||
<tr>
|
||||
{headers.map((header, index) => {
|
||||
const { width, isFixed, children: thChildren, className } = header;
|
||||
const validWidth = isFixed ? width : (containerWidth - fixedWidth) * width;
|
||||
return (<th key={index} style={{ width: validWidth }} className={className}>{thChildren}</th>);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{children}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
FixedWidthTable.propTypes = {
|
||||
className: PropTypes.string,
|
||||
headers: PropTypes.array,
|
||||
theadOptions: PropTypes.object,
|
||||
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.number]),
|
||||
};
|
||||
|
||||
export default FixedWidthTable;
|
@@ -1,47 +1,27 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import RepoItem from './repo-item';
|
||||
import { gettext, trashReposExpireDays } from '../../../utils/constants';
|
||||
import FixedWidthTable from '../../common/fixed-width-table';
|
||||
|
||||
const Repos = ({ repos, filterRestoredRepo }) => {
|
||||
const [containerWidth, setContainerWidth] = useState(0);
|
||||
|
||||
const containerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const handleResize = () => {
|
||||
if (!container) return;
|
||||
setContainerWidth(container.offsetWidth);
|
||||
};
|
||||
const resizeObserver = new ResizeObserver(handleResize);
|
||||
container && resizeObserver.observe(container);
|
||||
|
||||
return () => {
|
||||
container && resizeObserver.unobserve(container);
|
||||
};
|
||||
}, []);
|
||||
const headers = useMemo(() => [
|
||||
{ width: 40, isFixed: true, className: 'pl-2 pr-2' },
|
||||
{ width: 0.5, isFixed: false, children: gettext('Name') },
|
||||
{ width: 0.3, isFixed: false, children: gettext('Deleted Time') },
|
||||
{ width: 0.2, isFixed: false },
|
||||
], []);
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<div>
|
||||
<p className="tip my-deleted-repos-tip">{gettext('Tip: libraries deleted {placeholder} days ago will be cleaned automatically.').replace('{placeholder}', trashReposExpireDays)}</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 40 }} className="pl-2 pr-2">{/* img*/}</th>
|
||||
<th style={{ width: (containerWidth - 40) * 0.5 }}>{gettext('Name')}</th>
|
||||
<th style={{ width: (containerWidth - 40) * 0.3 }}>{gettext('Deleted Time')}</th>
|
||||
<th style={{ width: (containerWidth - 40) * 0.2 }}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{repos.map((repo) => {
|
||||
return (
|
||||
<RepoItem key={repo.repo_id} repo={repo} filterRestoredRepo={filterRestoredRepo} />
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<FixedWidthTable headers={headers} >
|
||||
{repos.map((repo) => {
|
||||
return (
|
||||
<RepoItem key={repo.repo_id} repo={repo} filterRestoredRepo={filterRestoredRepo} />
|
||||
);
|
||||
})}
|
||||
</FixedWidthTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -1,51 +1,31 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
import FolderRecords from './folder-records';
|
||||
import FileRecords from './file-records';
|
||||
import FixedWidthTable from '../../../common/fixed-width-table';
|
||||
|
||||
const Table = ({ repoID, renderFolder, data }) => {
|
||||
const [containerWidth, setContainerWidth] = useState(0);
|
||||
|
||||
const containerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const handleResize = () => {
|
||||
if (!container) return;
|
||||
setContainerWidth(container.offsetWidth);
|
||||
};
|
||||
const resizeObserver = new ResizeObserver(handleResize);
|
||||
container && resizeObserver.observe(container);
|
||||
|
||||
return () => {
|
||||
container && resizeObserver.unobserve(container);
|
||||
};
|
||||
}, []);
|
||||
const headers = useMemo(() => [
|
||||
{ isFixed: true, width: 40, className: 'pl-2 pr-2' },
|
||||
{ isFixed: false, width: 0.25, children: gettext('Name') },
|
||||
{ isFixed: false, width: 0.4, children: gettext('Original path') },
|
||||
{ isFixed: false, width: 0.12, children: gettext('Delete Time') },
|
||||
{ isFixed: false, width: 0.13, children: gettext('Size') },
|
||||
{ isFixed: false, width: 0.1, children: gettext('Size') },
|
||||
], []);
|
||||
|
||||
const { items, showFolder, commitID, baseDir, folderPath, folderItems } = data;
|
||||
|
||||
return (
|
||||
<div className="table-container p-0" ref={containerRef}>
|
||||
<table className="table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 40 }} className="pl-2 pr-2">{/* icon */}</th>
|
||||
<th style={{ width: (containerWidth - 40) * 0.25 }}>{gettext('Name')}</th>
|
||||
<th style={{ width: (containerWidth - 40) * 0.4 }}>{gettext('Original path')}</th>
|
||||
<th style={{ width: (containerWidth - 40) * 0.12 }}>{gettext('Delete Time')}</th>
|
||||
<th style={{ width: (containerWidth - 40) * 0.13 }}>{gettext('Size')}</th>
|
||||
<th style={{ width: (containerWidth - 40) * 0.1 }}>{/* op */}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{showFolder ? (
|
||||
<FolderRecords records={folderItems} repoID={repoID} commitID={commitID} baseDir={baseDir} folderPath={folderPath} renderFolder={renderFolder} />
|
||||
) : (
|
||||
<FileRecords records={items} repoID={repoID} renderFolder={renderFolder} />
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="table-container p-0">
|
||||
<FixedWidthTable className="table-hover" headers={headers}>
|
||||
{showFolder ? (
|
||||
<FolderRecords records={folderItems} repoID={repoID} commitID={commitID} baseDir={baseDir} folderPath={folderPath} renderFolder={renderFolder} />
|
||||
) : (
|
||||
<FileRecords records={items} repoID={repoID} renderFolder={renderFolder} />
|
||||
)}
|
||||
</FixedWidthTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -10,9 +10,11 @@ import { METADATA_MODE, TAGS_MODE } from '../dir-view-mode/constants';
|
||||
|
||||
const Detail = React.memo(({ repoID, path, currentMode, dirent, currentRepoInfo, repoTags, fileTags, onClose, onFileTagChanged }) => {
|
||||
const isView = useMemo(() => currentMode === METADATA_MODE || path.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES), [currentMode, path]);
|
||||
const isTag = useMemo(() => currentMode === TAGS_MODE || path.startsWith('/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES), [currentMode, path]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isView) return;
|
||||
if (isTag) return;
|
||||
|
||||
// init context
|
||||
const context = new MetadataContext();
|
||||
@@ -24,9 +26,9 @@ const Detail = React.memo(({ repoID, path, currentMode, dirent, currentRepoInfo,
|
||||
delete window['sfMetadataContext'];
|
||||
}
|
||||
};
|
||||
}, [repoID, currentRepoInfo, isView]);
|
||||
}, [repoID, currentRepoInfo, isView, isTag]);
|
||||
|
||||
if (currentMode === TAGS_MODE) return null;
|
||||
if (isTag) return null;
|
||||
|
||||
if (isView) {
|
||||
const viewId = path.split('/').pop();
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { siteRoot, gettext, username, enableSeadoc, thumbnailSizeForOriginal, thumbnailDefaultSize, fileServerRoot } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import TextTranslation from '../../utils/text-translation';
|
||||
@@ -20,6 +21,7 @@ import { EVENT_BUS_TYPE } from '../common/event-bus-type';
|
||||
import EmptyTip from '../empty-tip';
|
||||
import imageAPI from '../../utils/image-api';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import FixedWidthTable from '../common/fixed-width-table';
|
||||
|
||||
const propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
@@ -80,7 +82,6 @@ class DirentListView extends React.Component {
|
||||
activeDirent: null,
|
||||
isListDropTipShow: false,
|
||||
isShowDirentsDraggablePreview: false,
|
||||
containerWidth: 0,
|
||||
};
|
||||
|
||||
this.enteredCounter = 0; // Determine whether to enter the child element to avoid dragging bubbling bugs。
|
||||
@@ -101,20 +102,12 @@ class DirentListView extends React.Component {
|
||||
const { modify } = customPermission.permission;
|
||||
this.canDrop = modify;
|
||||
}
|
||||
|
||||
this.containerRef = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubscribeEvent = this.props.eventBus.subscribe(EVENT_BUS_TYPE.RESTORE_IMAGE, this.recalculateImageItems);
|
||||
this.resizeObserver = new ResizeObserver(this.handleResize);
|
||||
this.containerRef && this.resizeObserver.observe(this.containerRef);
|
||||
}
|
||||
|
||||
handleResize = () => {
|
||||
this.setState({ containerWidth: this.containerRef.offsetWidth - 32 });
|
||||
};
|
||||
|
||||
recalculateImageItems = () => {
|
||||
if (!this.state.isImagePopupOpen) return;
|
||||
let imageItems = this.props.direntList
|
||||
@@ -129,7 +122,6 @@ class DirentListView extends React.Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribeEvent();
|
||||
this.containerRef && this.resizeObserver.unobserve(this.containerRef);
|
||||
}
|
||||
|
||||
freezeItem = () => {
|
||||
@@ -687,18 +679,56 @@ class DirentListView extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
getHeaders = (isDesktop) => {
|
||||
const { direntList, sortBy, sortOrder } = this.props;
|
||||
const { containerWidth } = this.state;
|
||||
if (!isDesktop) {
|
||||
return [
|
||||
{ isFixed: false, width: 0.12 },
|
||||
{ isFixed: false, width: 0.8 },
|
||||
{ isFixed: false, width: 0.08 },
|
||||
];
|
||||
}
|
||||
|
||||
// sort
|
||||
const sortByName = sortBy == 'name';
|
||||
const sortByTime = sortBy == 'time';
|
||||
const sortBySize = sortBy == 'size';
|
||||
const sortIcon = sortOrder == 'asc' ? <span className="sf3-font sf3-font-down rotate-180 d-inline-block"></span> : <span className="sf3-font sf3-font-down"></span>;
|
||||
return [
|
||||
{ isFixed: true, width: 31, className: 'pl10 pr-2', children: (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="vam"
|
||||
onChange={this.props.onAllItemSelected}
|
||||
checked={this.props.isAllItemSelected}
|
||||
title={this.props.isAllItemSelected ? gettext('Unselect all items') : gettext('Select all items')}
|
||||
aria-label={this.props.isAllItemSelected ? gettext('Unselect all items') : gettext('Select all items')}
|
||||
disabled={direntList.length === 0}
|
||||
/>
|
||||
) }, {
|
||||
isFixed: true, width: 32, className: 'pl-2 pr-2', // star
|
||||
}, {
|
||||
isFixed: true, width: 40, className: 'pl-2 pr-2', // icon
|
||||
}, {
|
||||
isFixed: false, width: 0.5, children: (<a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {sortByName && sortIcon}</a>),
|
||||
}, {
|
||||
isFixed: false, width: 0.06, // tag
|
||||
}, {
|
||||
isFixed: false, width: 0.18, // operation
|
||||
}, {
|
||||
isFixed: false, width: 0.11, children: (<a className="d-block table-sort-op" href="#" onClick={this.sortBySize}>{gettext('Size')} {sortBySize && sortIcon}</a>)
|
||||
}, {
|
||||
isFixed: false, width: 0.15, children: (<a className="d-block table-sort-op" href="#" onClick={this.sortByTime}>{gettext('Last Update')} {sortByTime && sortIcon}</a>)
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
render() {
|
||||
const { direntList } = this.props;
|
||||
|
||||
const isDesktop = Utils.isDesktop();
|
||||
const repoEncrypted = this.props.currentRepoInfo.encrypted;
|
||||
const headers = this.getHeaders(isDesktop);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -710,43 +740,13 @@ class DirentListView extends React.Component {
|
||||
onDragOver={this.onTableDragOver}
|
||||
onDragLeave={this.onTableDragLeave}
|
||||
onDrop={this.tableDrop}
|
||||
ref={ref => this.containerRef = ref}
|
||||
>
|
||||
{direntList.length > 0 &&
|
||||
<table className={`table-hover ${isDesktop ? '' : 'table-thead-hidden'}`}>
|
||||
{isDesktop ? (
|
||||
<thead onMouseDown={this.onThreadMouseDown} onContextMenu={this.onThreadContextMenu}>
|
||||
<tr>
|
||||
<th style={{ width: 31 }} className="pl10 pr-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="vam"
|
||||
onChange={this.props.onAllItemSelected}
|
||||
checked={this.props.isAllItemSelected}
|
||||
title={this.props.isAllItemSelected ? gettext('Unselect all items') : gettext('Select all items')}
|
||||
aria-label={this.props.isAllItemSelected ? gettext('Unselect all items') : gettext('Select all items')}
|
||||
disabled={direntList.length === 0}
|
||||
/>
|
||||
</th>
|
||||
<th style={{ width: 32 }} className="pl-2 pr-2">{/* star */}</th>
|
||||
<th style={{ width: 40 }} className="pl-2 pr-2">{/* icon */}</th>
|
||||
<th style={{ width: (containerWidth - 103) * 0.5 }}><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {sortByName && sortIcon}</a></th>
|
||||
<th style={{ width: (containerWidth - 103) * 0.06 }}>{/* tag */}</th>
|
||||
<th style={{ width: (containerWidth - 103) * 0.18 }}>{/* operation */}</th>
|
||||
<th style={{ width: (containerWidth - 103) * 0.11 }}><a className="d-block table-sort-op" href="#" onClick={this.sortBySize}>{gettext('Size')} {sortBySize && sortIcon}</a></th>
|
||||
<th style={{ width: (containerWidth - 103) * 0.15 }}><a className="d-block table-sort-op" href="#" onClick={this.sortByTime}>{gettext('Last Update')} {sortByTime && sortIcon}</a></th>
|
||||
</tr>
|
||||
</thead>
|
||||
) : (
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="12%"></th>
|
||||
<th width="80%"></th>
|
||||
<th width="8%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
)}
|
||||
<tbody>
|
||||
{direntList.length > 0 && (
|
||||
<FixedWidthTable
|
||||
className={classnames('table-hover', { 'table-thead-hidden': !isDesktop })}
|
||||
headers={headers}
|
||||
theadOptions={isDesktop ? { onMouseDown: this.onThreadMouseDown, onContextMenu: this.onThreadContextMenu } : {}}
|
||||
>
|
||||
{direntList.map((dirent, index) => {
|
||||
return (
|
||||
<DirentListItem
|
||||
@@ -790,9 +790,8 @@ class DirentListView extends React.Component {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</FixedWidthTable>
|
||||
)}
|
||||
{direntList.length === 0 &&
|
||||
<EmptyTip text={gettext('No file')}/>
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ import React, { Fragment, Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from '@gatsbyjs/reach-router';
|
||||
import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
|
||||
import classnames from 'classnames';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { gettext, siteRoot, isPro } from '../../utils/constants';
|
||||
@@ -11,6 +12,7 @@ import toaster from '../../components/toast';
|
||||
import SharePermissionEditor from '../../components/select-editor/share-permission-editor';
|
||||
import SharedFolderInfo from '../../models/shared-folder-info';
|
||||
import PermSelect from '../../components/dialog/perm-select';
|
||||
import FixedWidthTable from '../../components/common/fixed-width-table';
|
||||
|
||||
class Content extends Component {
|
||||
|
||||
@@ -26,51 +28,46 @@ class Content extends Component {
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
} else if (errorMsg) {
|
||||
}
|
||||
if (errorMsg) {
|
||||
return <p className="error text-center">{errorMsg}</p>;
|
||||
} else {
|
||||
const emptyTip = (
|
||||
}
|
||||
if (!items.length) {
|
||||
return (
|
||||
<EmptyTip
|
||||
title={gettext('No folders shared')}
|
||||
text={gettext('You have not shared any folders with other users yet. You can share a folder with other users by clicking the share icon to the right of a folder\'s name.')}
|
||||
>
|
||||
</EmptyTip>
|
||||
);
|
||||
|
||||
// sort
|
||||
const sortByName = sortBy == 'name';
|
||||
const sortIcon = sortOrder == 'asc' ? <span className="sf3-font sf3-font-down rotate-180 d-inline-block"></span> : <span className="sf3-font sf3-font-down"></span>;
|
||||
|
||||
const isDesktop = Utils.isDesktop();
|
||||
const table = (
|
||||
<table className={`table-hover ${isDesktop ? '' : 'table-thead-hidden'}`}>
|
||||
<thead>
|
||||
{isDesktop ? (
|
||||
<tr>
|
||||
<th width="4%">{/* icon*/}</th>
|
||||
<th width="34%"><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {sortByName && sortIcon}</a></th>
|
||||
<th width="30%">{gettext('Share To')}</th>
|
||||
<th width="24%">{gettext('Permission')}</th>
|
||||
<th width="8%"></th>
|
||||
</tr>
|
||||
) : (
|
||||
<tr>
|
||||
<th width="12%"></th>
|
||||
<th width="80%"></th>
|
||||
<th width="8%"></th>
|
||||
</tr>
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
return (<Item key={index} isDesktop={isDesktop} item={item} />);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
return items.length ? table : emptyTip;
|
||||
}
|
||||
|
||||
// sort
|
||||
const sortByName = sortBy == 'name';
|
||||
const sortIcon = sortOrder == 'asc' ? <span className="sf3-font sf3-font-down rotate-180 d-inline-block"></span> : <span className="sf3-font sf3-font-down"></span>;
|
||||
|
||||
const isDesktop = Utils.isDesktop();
|
||||
|
||||
return (
|
||||
<FixedWidthTable
|
||||
className={classnames('table-hover', { 'table-thead-hidden': !isDesktop })}
|
||||
headers={isDesktop ? [
|
||||
{ isFixed: true, width: 40 }, // icon
|
||||
{ isFixed: false, width: 0.35, children: (<a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {sortByName && sortIcon}</a>) },
|
||||
{ isFixed: false, width: 0.3, children: gettext('Share To') },
|
||||
{ isFixed: false, width: 0.25, children: gettext('Permission') },
|
||||
{ isFixed: false, width: 0.1 },
|
||||
] : [
|
||||
{ isFixed: false, width: 0.12 },
|
||||
{ isFixed: false, width: 0.8 },
|
||||
{ isFixed: false, width: 0.08 },
|
||||
]}
|
||||
>
|
||||
{items.map((item, index) => {
|
||||
return (<Item key={index} isDesktop={isDesktop} item={item} />);
|
||||
})}
|
||||
</FixedWidthTable>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +211,7 @@ class Item extends Component {
|
||||
if (this.props.isDesktop) {
|
||||
return (
|
||||
<tr onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onFocus={this.onMouseEnter}>
|
||||
<td><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td>
|
||||
<td className="pl10 pr-2"><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td>
|
||||
<td><Link to={folderUrl}>{item.folder_name}</Link></td>
|
||||
<td>
|
||||
{item.share_type == 'personal' ?
|
||||
|
@@ -2,6 +2,7 @@ import React, { Fragment, Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from '@gatsbyjs/reach-router';
|
||||
import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
|
||||
import classnames from 'classnames';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { gettext, siteRoot, isPro } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
@@ -10,6 +11,7 @@ import EmptyTip from '../../components/empty-tip';
|
||||
import SharePermissionEditor from '../../components/select-editor/share-permission-editor';
|
||||
import SharedRepoInfo from '../../models/shared-repo-info';
|
||||
import PermSelect from '../../components/dialog/perm-select';
|
||||
import FixedWidthTable from '../../components/common/fixed-width-table';
|
||||
|
||||
class Content extends Component {
|
||||
|
||||
@@ -25,55 +27,46 @@ class Content extends Component {
|
||||
|
||||
if (loading) {
|
||||
return <span className="loading-icon loading-tip"></span>;
|
||||
} else if (errorMsg) {
|
||||
}
|
||||
if (errorMsg) {
|
||||
return <p className="error text-center">{errorMsg}</p>;
|
||||
} else {
|
||||
const emptyTip = (
|
||||
}
|
||||
|
||||
if (!items.length) {
|
||||
return (
|
||||
<EmptyTip
|
||||
title={gettext('No libraries shared')}
|
||||
text={gettext('You have not shared any libraries with other users yet. You can share a library with other users by clicking the share icon to the right of a library\'s name in "My Libraries".')}
|
||||
>
|
||||
</EmptyTip>
|
||||
);
|
||||
|
||||
// sort
|
||||
const sortByName = sortBy == 'name';
|
||||
const sortIcon = sortOrder == 'asc' ? <span className="sf3-font sf3-font-down rotate-180 d-inline-block"></span> : <span className="sf3-font sf3-font-down"></span>;
|
||||
|
||||
const isDesktop = Utils.isDesktop();
|
||||
const table = (
|
||||
<table className={`table-hover ${isDesktop ? '' : 'table-thead-hidden'}`}>
|
||||
<thead>
|
||||
{isDesktop ? (
|
||||
<tr>
|
||||
<th width="4%">{/* icon*/}</th>
|
||||
<th width="34%"><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {sortByName && sortIcon}</a></th>
|
||||
<th width="30%">{gettext('Share To')}</th>
|
||||
<th width="24%">{gettext('Permission')}</th>
|
||||
<th width="8%"></th>
|
||||
</tr>
|
||||
) : (
|
||||
<tr>
|
||||
<th width="12%"></th>
|
||||
<th width="80%"></th>
|
||||
<th width="8%"></th>
|
||||
</tr>
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
return (<Item
|
||||
key={index}
|
||||
isDesktop={isDesktop}
|
||||
item={item}
|
||||
/>);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
return items.length ? table : emptyTip;
|
||||
}
|
||||
|
||||
// sort
|
||||
const sortByName = sortBy == 'name';
|
||||
const sortIcon = sortOrder == 'asc' ? <span className="sf3-font sf3-font-down rotate-180 d-inline-block"></span> : <span className="sf3-font sf3-font-down"></span>;
|
||||
|
||||
const isDesktop = Utils.isDesktop();
|
||||
return (
|
||||
<FixedWidthTable
|
||||
className={classnames('table-hover', { 'table-thead-hidden': !isDesktop })}
|
||||
headers={isDesktop ? [
|
||||
{ isFixed: true, width: 40 },
|
||||
{ isFixed: false, width: 0.35, children: (<a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {sortByName && sortIcon}</a>) },
|
||||
{ isFixed: false, width: 0.3, children: gettext('Share To') },
|
||||
{ isFixed: false, width: 0.25, children: gettext('Permission') },
|
||||
{ isFixed: false, width: 0.1 },
|
||||
] : [
|
||||
{ isFixed: false, width: 0.12 },
|
||||
{ isFixed: false, width: 0.8 },
|
||||
{ isFixed: false, width: 0.08 },
|
||||
]}
|
||||
>
|
||||
{items.map((item, index) => {
|
||||
return (<Item key={index} isDesktop={isDesktop} item={item} />);
|
||||
})}
|
||||
</FixedWidthTable>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,7 +221,7 @@ class Item extends Component {
|
||||
if (this.props.isDesktop) {
|
||||
return (
|
||||
<tr onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onFocus={this.onMouseEnter}>
|
||||
<td><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td>
|
||||
<td className="pl10 pr-2"><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td>
|
||||
<td><Link to={repoUrl}>{item.repo_name}</Link></td>
|
||||
<td>
|
||||
{item.share_type == 'personal' ? <span title={item.contact_email}>{shareTo}</span> : shareTo}
|
||||
|
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { Link } from '@gatsbyjs/reach-router';
|
||||
import dayjs from 'dayjs';
|
||||
import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
|
||||
import classnames from 'classnames';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { isPro, gettext, siteRoot, canGenerateUploadLink } from '../../utils/constants';
|
||||
@@ -16,6 +17,7 @@ import SortOptionsDialog from '../../components/dialog/sort-options';
|
||||
import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog';
|
||||
import Selector from '../../components/single-selector';
|
||||
import SingleDropdownToolbar from '../../components/toolbar/single-dropdown-toolbar';
|
||||
import FixedWidthTable from '../../components/common/fixed-width-table';
|
||||
|
||||
const contentPropTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
@@ -60,69 +62,60 @@ class Content extends Component {
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
} else if (errorMsg) {
|
||||
}
|
||||
if (errorMsg) {
|
||||
return <p className="error text-center">{errorMsg}</p>;
|
||||
} else {
|
||||
const emptyTip = (
|
||||
}
|
||||
if (!items.length) {
|
||||
return (
|
||||
<EmptyTip
|
||||
title={gettext('No share links')}
|
||||
text={gettext('You have not created any share links yet. A share link can be used to share files and folders with anyone. You can create a share link for a file or folder by clicking the share icon to the right of its name.')}
|
||||
>
|
||||
</EmptyTip>
|
||||
/>
|
||||
);
|
||||
|
||||
// sort
|
||||
const sortByName = sortBy == 'name';
|
||||
const sortByTime = sortBy == 'time';
|
||||
const sortIcon = sortOrder == 'asc' ? <span className="sf3-font sf3-font-down rotate-180 d-inline-block"></span> : <span className="sf3-font sf3-font-down"></span>;
|
||||
|
||||
const isDesktop = Utils.isDesktop();
|
||||
// only for some columns
|
||||
const columnWidths = isPro ? ['14%', '7%', '14%'] : ['21%', '14%', '20%'];
|
||||
const table = (
|
||||
<table className={`${isDesktop ? '' : 'table-thead-hidden'}`}>
|
||||
<thead>
|
||||
{isDesktop ? (
|
||||
<tr>
|
||||
<th width="4%">{/* icon*/}</th>
|
||||
<th width="31%"><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {sortByName && sortIcon}</a></th>
|
||||
<th width={columnWidths[0]}>{gettext('Library')}</th>
|
||||
{isPro && <th width="20%">{gettext('Permission')}</th>}
|
||||
<th width={columnWidths[1]}>{gettext('Visits')}</th>
|
||||
<th width={columnWidths[2]}><a className="d-block table-sort-op" href="#" onClick={this.sortByTime}>{gettext('Expiration')} {sortByTime && sortIcon}</a></th>
|
||||
<th width="10%">{/* Operations*/}</th>
|
||||
</tr>
|
||||
) : (
|
||||
<tr>
|
||||
<th width="12%"></th>
|
||||
<th width="80%"></th>
|
||||
<th width="8%"></th>
|
||||
</tr>
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
return (<Item
|
||||
key={index}
|
||||
isDesktop={isDesktop}
|
||||
item={item}
|
||||
onRemoveLink={this.props.onRemoveLink}
|
||||
isItemFreezed={this.state.isItemFreezed}
|
||||
toggleItemFreezed={this.toggleItemFreezed}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
return items.length ? (
|
||||
<>
|
||||
{table}
|
||||
{this.props.isLoadingMore && <div className="flex-shrink-0"><Loading /></div>}
|
||||
</>
|
||||
) : emptyTip;
|
||||
}
|
||||
|
||||
// sort
|
||||
const sortByName = sortBy == 'name';
|
||||
const sortByTime = sortBy == 'time';
|
||||
const sortIcon = sortOrder == 'asc' ? <span className="sf3-font sf3-font-down rotate-180 d-inline-block"></span> : <span className="sf3-font sf3-font-down"></span>;
|
||||
|
||||
const isDesktop = Utils.isDesktop();
|
||||
// only for some columns
|
||||
const columnWidths = isPro ? [0.14, 0.07, 0.14] : [0.21, 0.14, 0.2];
|
||||
|
||||
return (
|
||||
<>
|
||||
<FixedWidthTable
|
||||
className={classnames('', { 'table-thead-hidden': !isDesktop })}
|
||||
headers={isDesktop ? [
|
||||
{ isFixed: true, width: 40 }, // icon
|
||||
{ isFixed: false, width: 0.35, children: (<a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {sortByName && sortIcon}</a>) },
|
||||
{ isFixed: false, width: columnWidths[0], children: gettext('Library') },
|
||||
isPro ? { isFixed: false, width: 0.2, children: gettext('Permission') } : null,
|
||||
{ isFixed: false, width: columnWidths[1], children: gettext('Visits') },
|
||||
{ isFixed: false, width: columnWidths[2], children: (<a className="d-block table-sort-op" href="#" onClick={this.sortByTime}>{gettext('Expiration')} {sortByTime && sortIcon}</a>) },
|
||||
{ isFixed: false, width: 0.1 }, // operations
|
||||
].filter(i => i) : [
|
||||
{ isFixed: false, width: 0.12 },
|
||||
{ isFixed: false, width: 0.8 },
|
||||
{ isFixed: false, width: 0.08 },
|
||||
]}
|
||||
>
|
||||
{items.map((item, index) => {
|
||||
return (<Item
|
||||
key={index}
|
||||
isDesktop={isDesktop}
|
||||
item={item}
|
||||
onRemoveLink={this.props.onRemoveLink}
|
||||
isItemFreezed={this.state.isItemFreezed}
|
||||
toggleItemFreezed={this.toggleItemFreezed}
|
||||
/>);
|
||||
})}
|
||||
</FixedWidthTable>
|
||||
{this.props.isLoadingMore && <div className="flex-shrink-0"><Loading /></div>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +270,7 @@ class Item extends Component {
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
onFocus={this.handleMouseEnter}
|
||||
>
|
||||
<td><img src={iconUrl} width="24" alt="" /></td>
|
||||
<td className="pl-2 pr-2"><img src={iconUrl} width="24" alt="" /></td>
|
||||
<td>
|
||||
{item.is_dir ?
|
||||
<Link to={objUrl}>{item.obj_name}</Link> :
|
||||
|
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { Link } from '@gatsbyjs/reach-router';
|
||||
import dayjs from 'dayjs';
|
||||
import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
|
||||
import classnames from 'classnames';
|
||||
import { gettext, siteRoot, canGenerateShareLink } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
@@ -13,6 +14,7 @@ import UploadLink from '../../models/upload-link';
|
||||
import ShareAdminLink from '../../components/dialog/share-admin-link';
|
||||
import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog';
|
||||
import SingleDropdownToolbar from '../../components/toolbar/single-dropdown-toolbar';
|
||||
import FixedWidthTable from '../../components/common/fixed-width-table';
|
||||
|
||||
const contentPropTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
@@ -32,45 +34,37 @@ class Content extends Component {
|
||||
if (errorMsg) {
|
||||
return <p className="error text-center">{errorMsg}</p>;
|
||||
}
|
||||
|
||||
const emptyTip = (
|
||||
<EmptyTip
|
||||
title={gettext('No upload links')}
|
||||
text={gettext('You have not created any upload links yet. An upload link allows anyone to upload files to a folder or library. You can create an upload link for a folder or library by clicking the share icon to the right of its name.')}
|
||||
>
|
||||
</EmptyTip>
|
||||
);
|
||||
if (!items.length) {
|
||||
return (
|
||||
<EmptyTip
|
||||
title={gettext('No upload links')}
|
||||
text={gettext('You have not created any upload links yet. An upload link allows anyone to upload files to a folder or library. You can create an upload link for a folder or library by clicking the share icon to the right of its name.')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const isDesktop = Utils.isDesktop();
|
||||
const table = (
|
||||
<table className={`table-hover ${isDesktop ? '' : 'table-thead-hidden'}`}>
|
||||
<thead>
|
||||
{isDesktop ? (
|
||||
<tr>
|
||||
<th width="4%">{/* icon*/}</th>
|
||||
<th width="30%">{gettext('Name')}</th>
|
||||
<th width="24%">{gettext('Library')}</th>
|
||||
<th width="16%">{gettext('Visits')}</th>
|
||||
<th width="16%">{gettext('Expiration')}</th>
|
||||
<th width="10%">{/* Operations*/}</th>
|
||||
</tr>
|
||||
) : (
|
||||
<tr>
|
||||
<th width="12%"></th>
|
||||
<th width="80%"></th>
|
||||
<th width="8%"></th>
|
||||
</tr>
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
return (<Item key={index} isDesktop={isDesktop} item={item} onRemoveLink={this.props.onRemoveLink}/>);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
return (
|
||||
<FixedWidthTable
|
||||
className={classnames('table-hover', { 'table-thead-hidden': !isDesktop })}
|
||||
headers={isDesktop ? [
|
||||
{ isFixed: true, width: 40 }, // icon
|
||||
{ isFixed: false, width: 0.33, children: gettext('Name') },
|
||||
{ isFixed: false, width: 0.25, children: gettext('Library') },
|
||||
{ isFixed: false, width: 0.16, children: gettext('Visits') },
|
||||
{ isFixed: false, width: 0.16, children: gettext('Expiration') },
|
||||
{ isFixed: false, width: 0.1 }, // Operations
|
||||
] : [
|
||||
{ isFixed: false, width: 0.12 },
|
||||
{ isFixed: false, width: 0.8 },
|
||||
{ isFixed: false, width: 0.08 },
|
||||
]}
|
||||
>
|
||||
{items.map((item, index) => {
|
||||
return (<Item key={index} isDesktop={isDesktop} item={item} onRemoveLink={this.props.onRemoveLink}/>);
|
||||
})}
|
||||
</FixedWidthTable>
|
||||
);
|
||||
|
||||
return items.length ? table : emptyTip;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +139,7 @@ class Item extends Component {
|
||||
<Fragment>
|
||||
{this.props.isDesktop ?
|
||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut} onFocus={this.handleMouseOver}>
|
||||
<td><img src={iconUrl} alt="" width="24" /></td>
|
||||
<td className="pl-2 pr-2"><img src={iconUrl} alt="" width="24" /></td>
|
||||
<td>
|
||||
<Link to={objUrl}>{item.obj_name}</Link>
|
||||
{item.obj_id === '' ? <span style={{ color: 'red' }}>{gettext('(deleted)')}</span> : null}
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import React, { useCallback, useState, useRef, useEffect } from 'react';
|
||||
import React, { useCallback, useState, useRef } from 'react';
|
||||
import { useTagView, useTags } from '../../hooks';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import TagFile from './tag-file';
|
||||
import { getRecordIdFromRecord } from '../../../metadata/utils/cell';
|
||||
import EmptyTip from '../../../components/empty-tip';
|
||||
import ImagePreviewer from '../../../metadata/components/cell-formatter/image-previewer';
|
||||
import FixedWidthTable from '../../../components/common/fixed-width-table';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -13,10 +14,8 @@ const TagFiles = () => {
|
||||
const { tagsData } = useTags();
|
||||
const [selectedFiles, setSelectedFiles] = useState(null);
|
||||
const [isImagePreviewerVisible, setImagePreviewerVisible] = useState(false);
|
||||
const [containerWidth, setContainerWidth] = useState(0);
|
||||
|
||||
const currentImageRef = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
|
||||
const onMouseDown = useCallback((event) => {
|
||||
if (event.button === 2) {
|
||||
@@ -66,68 +65,77 @@ const TagFiles = () => {
|
||||
setImagePreviewerVisible(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const handleResize = () => {
|
||||
if (!container) return;
|
||||
// 32: container padding left + container padding right
|
||||
setContainerWidth(container.offsetWidth - 32);
|
||||
};
|
||||
const resizeObserver = new ResizeObserver(handleResize);
|
||||
container && resizeObserver.observe(container);
|
||||
|
||||
return () => {
|
||||
container && resizeObserver.unobserve(container);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (tagFiles.rows.length === 0) {
|
||||
return (<EmptyTip text={gettext('No files')} />);
|
||||
}
|
||||
|
||||
const isSelectedAll = selectedFiles && selectedFiles.length === tagFiles.rows.length;
|
||||
const headers = [
|
||||
{
|
||||
isFixed: true,
|
||||
width: 31,
|
||||
className: 'pl10 pr-2',
|
||||
children: (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="vam"
|
||||
onChange={onSelectedAll}
|
||||
checked={isSelectedAll}
|
||||
title={isSelectedAll ? gettext('Unselect all') : gettext('Select all')}
|
||||
disabled={tagFiles.rows.length === 0}
|
||||
/>
|
||||
)
|
||||
}, {
|
||||
isFixed: true,
|
||||
width: 41,
|
||||
className: 'pl-2 pr-2',
|
||||
}, {
|
||||
isFixed: false,
|
||||
width: 0.5,
|
||||
children: (<a className="d-block table-sort-op" href="#">{gettext('Name')}</a>),
|
||||
}, {
|
||||
isFixed: false,
|
||||
width: 0.06,
|
||||
}, {
|
||||
isFixed: false,
|
||||
width: 0.18,
|
||||
}, {
|
||||
isFixed: false,
|
||||
width: 0.11,
|
||||
children: (<a className="d-block table-sort-op" href="#">{gettext('Size')}</a>),
|
||||
}, {
|
||||
isFixed: false,
|
||||
width: 0.15,
|
||||
children: (<a className="d-block table-sort-op" href="#">{gettext('Last Update')}</a>),
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="table-container" ref={containerRef}>
|
||||
<table className="table-hover">
|
||||
<thead onMouseDown={onThreadMouseDown} onContextMenu={onThreadContextMenu}>
|
||||
<tr>
|
||||
<th style={{ width: 31 }} className="pl10 pr-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="vam"
|
||||
onChange={onSelectedAll}
|
||||
checked={isSelectedAll}
|
||||
title={isSelectedAll ? gettext('Unselect all') : gettext('Select all')}
|
||||
disabled={tagFiles.rows.length === 0}
|
||||
/>
|
||||
</th>
|
||||
<th style={{ width: 40 }} className="pl-2 pr-2">{/* icon */}</th>
|
||||
<th style={{ width: (containerWidth - 71) * 0.5 }}><a className="d-block table-sort-op" href="#">{gettext('Name')}</a></th>
|
||||
<th style={{ width: (containerWidth - 71) * 0.06 }}>{/* tag */}</th>
|
||||
<th style={{ width: (containerWidth - 71) * 0.18 }}>{/* operation */}</th>
|
||||
<th style={{ width: (containerWidth - 71) * 0.11 }}><a className="d-block table-sort-op" href="#">{gettext('Size')}</a></th>
|
||||
<th style={{ width: (containerWidth - 71) * 0.15 }}><a className="d-block table-sort-op" href="#">{gettext('Last Update')}</a></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tagFiles.rows.map(file => {
|
||||
const fileId = getRecordIdFromRecord(file);
|
||||
return (
|
||||
<TagFile
|
||||
key={fileId}
|
||||
repoID={repoID}
|
||||
isSelected={selectedFiles && selectedFiles.includes(fileId)}
|
||||
file={file}
|
||||
tagsData={tagsData}
|
||||
onSelectFile={onSelectFile}
|
||||
reSelectFiles={reSelectFiles}
|
||||
openImagePreview={openImagePreview}
|
||||
/>);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="table-container">
|
||||
<FixedWidthTable
|
||||
headers={headers}
|
||||
className="table-hover"
|
||||
theadOptions={{
|
||||
onMouseDown: onThreadMouseDown,
|
||||
onContextMenu: onThreadContextMenu,
|
||||
}}
|
||||
>
|
||||
{tagFiles.rows.map(file => {
|
||||
const fileId = getRecordIdFromRecord(file);
|
||||
return (
|
||||
<TagFile
|
||||
key={fileId}
|
||||
repoID={repoID}
|
||||
isSelected={selectedFiles && selectedFiles.includes(fileId)}
|
||||
file={file}
|
||||
tagsData={tagsData}
|
||||
onSelectFile={onSelectFile}
|
||||
reSelectFiles={reSelectFiles}
|
||||
openImagePreview={openImagePreview}
|
||||
/>);
|
||||
})}
|
||||
</FixedWidthTable>
|
||||
</div>
|
||||
{isImagePreviewerVisible && (
|
||||
<ImagePreviewer
|
||||
|
Reference in New Issue
Block a user