diff --git a/frontend/src/components/common/fixed-width-table.js b/frontend/src/components/common/fixed-width-table.js new file mode 100644 index 00000000000..12fd4b11a28 --- /dev/null +++ b/frontend/src/components/common/fixed-width-table.js @@ -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 ( + + + + {headers.map((header, index) => { + const { width, isFixed, children: thChildren, className } = header; + const validWidth = isFixed ? width : (containerWidth - fixedWidth) * width; + return (); + })} + + + + {children} + +
{thChildren}
+ ); +}; + +FixedWidthTable.propTypes = { + className: PropTypes.string, + headers: PropTypes.array, + theadOptions: PropTypes.object, + children: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.number]), +}; + +export default FixedWidthTable; diff --git a/frontend/src/components/dialog/my-deleted-repos-dialog/repos.js b/frontend/src/components/dialog/my-deleted-repos-dialog/repos.js index e50daafe49b..47bddcea291 100644 --- a/frontend/src/components/dialog/my-deleted-repos-dialog/repos.js +++ b/frontend/src/components/dialog/my-deleted-repos-dialog/repos.js @@ -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 ( -
+

{gettext('Tip: libraries deleted {placeholder} days ago will be cleaned automatically.').replace('{placeholder}', trashReposExpireDays)}

- - - - - - - - - - - {repos.map((repo) => { - return ( - - ); - })} - -
{/* img*/}{gettext('Name')}{gettext('Deleted Time')}
+ + {repos.map((repo) => { + return ( + + ); + })} +
); }; diff --git a/frontend/src/components/dialog/trash-dialog/table/index.js b/frontend/src/components/dialog/trash-dialog/table/index.js index 61b538d6a3f..f1f3488d735 100644 --- a/frontend/src/components/dialog/trash-dialog/table/index.js +++ b/frontend/src/components/dialog/trash-dialog/table/index.js @@ -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 ( -
- - - - - - - - - - - - - {showFolder ? ( - - ) : ( - - )} - -
{/* icon */}{gettext('Name')}{gettext('Original path')}{gettext('Delete Time')}{gettext('Size')}{/* op */}
+
+ + {showFolder ? ( + + ) : ( + + )} +
); }; diff --git a/frontend/src/components/dirent-detail/index.js b/frontend/src/components/dirent-detail/index.js index 30f37d8b156..542f244cd54 100644 --- a/frontend/src/components/dirent-detail/index.js +++ b/frontend/src/components/dirent-detail/index.js @@ -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(); diff --git a/frontend/src/components/dirent-list-view/dirent-list-view.js b/frontend/src/components/dirent-list-view/dirent-list-view.js index 2e962b6039f..d7e4d426bf3 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-view.js +++ b/frontend/src/components/dirent-list-view/dirent-list-view.js @@ -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' ? : ; + return [ + { isFixed: true, width: 31, className: 'pl10 pr-2', children: ( + + ) }, { + 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: ({gettext('Name')} {sortByName && sortIcon}), + }, { + isFixed: false, width: 0.06, // tag + }, { + isFixed: false, width: 0.18, // operation + }, { + isFixed: false, width: 0.11, children: ({gettext('Size')} {sortBySize && sortIcon}) + }, { + isFixed: false, width: 0.15, children: ({gettext('Last Update')} {sortByTime && sortIcon}) + } + ]; + }; + + render() { + const { direntList } = this.props; const isDesktop = Utils.isDesktop(); const repoEncrypted = this.props.currentRepoInfo.encrypted; + const headers = this.getHeaders(isDesktop); return (
this.containerRef = ref} > - {direntList.length > 0 && - - {isDesktop ? ( - - - - - - - - - - - - - ) : ( - - - - - - - - )} - + {direntList.length > 0 && ( + {direntList.map((dirent, index) => { return ( ); })} - -
- - {/* star */}{/* icon */}{gettext('Name')} {sortByName && sortIcon}{/* tag */}{/* operation */}{gettext('Size')} {sortBySize && sortIcon}{gettext('Last Update')} {sortByTime && sortIcon}
- } + + )} {direntList.length === 0 && } diff --git a/frontend/src/pages/share-admin/folders.js b/frontend/src/pages/share-admin/folders.js index e5be54b27db..a468f08687c 100644 --- a/frontend/src/pages/share-admin/folders.js +++ b/frontend/src/pages/share-admin/folders.js @@ -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 ; - } else if (errorMsg) { + } + if (errorMsg) { return

{errorMsg}

; - } else { - const emptyTip = ( + } + if (!items.length) { + return ( ); + } - // sort - const sortByName = sortBy == 'name'; - const sortIcon = sortOrder == 'asc' ? : ; - - const isDesktop = Utils.isDesktop(); - const table = ( - - - {isDesktop ? ( - - - - - - - - ) : ( - - - - - - )} - - - {items.map((item, index) => { - return (); - })} - -
{/* icon*/}{gettext('Name')} {sortByName && sortIcon}{gettext('Share To')}{gettext('Permission')}
- ); + // sort + const sortByName = sortBy == 'name'; + const sortIcon = sortOrder == 'asc' ? : ; - return items.length ? table : emptyTip; - } + const isDesktop = Utils.isDesktop(); + + return ( + {gettext('Name')} {sortByName && sortIcon}) }, + { 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 (); + })} + + ); } } @@ -214,7 +211,7 @@ class Item extends Component { if (this.props.isDesktop) { return ( - {iconTitle} + {iconTitle} {item.folder_name} {item.share_type == 'personal' ? diff --git a/frontend/src/pages/share-admin/libraries.js b/frontend/src/pages/share-admin/libraries.js index c1ef78e7e6a..5fd531d40bc 100644 --- a/frontend/src/pages/share-admin/libraries.js +++ b/frontend/src/pages/share-admin/libraries.js @@ -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 ; - } else if (errorMsg) { + } + if (errorMsg) { return

{errorMsg}

; - } else { - const emptyTip = ( + } + + if (!items.length) { + return ( ); + } - // sort - const sortByName = sortBy == 'name'; - const sortIcon = sortOrder == 'asc' ? : ; - - const isDesktop = Utils.isDesktop(); - const table = ( - - - {isDesktop ? ( - - - - - - - - ) : ( - - - - - - )} - - - {items.map((item, index) => { - return (); - })} - -
{/* icon*/}{gettext('Name')} {sortByName && sortIcon}{gettext('Share To')}{gettext('Permission')}
- ); + // sort + const sortByName = sortBy == 'name'; + const sortIcon = sortOrder == 'asc' ? : ; - return items.length ? table : emptyTip; - } + const isDesktop = Utils.isDesktop(); + return ( + {gettext('Name')} {sortByName && sortIcon}) }, + { 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 (); + })} + + ); } } @@ -228,7 +221,7 @@ class Item extends Component { if (this.props.isDesktop) { return ( - {iconTitle} + {iconTitle} {item.repo_name} {item.share_type == 'personal' ? {shareTo} : shareTo} diff --git a/frontend/src/pages/share-admin/share-links.js b/frontend/src/pages/share-admin/share-links.js index f359ad1fa9a..4b7289c34b4 100644 --- a/frontend/src/pages/share-admin/share-links.js +++ b/frontend/src/pages/share-admin/share-links.js @@ -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 ; - } else if (errorMsg) { + } + if (errorMsg) { return

{errorMsg}

; - } else { - const emptyTip = ( + } + if (!items.length) { + return ( - + /> ); + } - // sort - const sortByName = sortBy == 'name'; - const sortByTime = sortBy == 'time'; - const sortIcon = sortOrder == 'asc' ? : ; - - const isDesktop = Utils.isDesktop(); - // only for some columns - const columnWidths = isPro ? ['14%', '7%', '14%'] : ['21%', '14%', '20%']; - const table = ( - - - {isDesktop ? ( - - - - - {isPro && } - - - - - ) : ( - - - - - - )} - - - {items.map((item, index) => { - return ( - ); - })} - -
{/* icon*/}{gettext('Name')} {sortByName && sortIcon}{gettext('Library')}{gettext('Permission')}{gettext('Visits')}{gettext('Expiration')} {sortByTime && sortIcon}{/* Operations*/}
- ); + // sort + const sortByName = sortBy == 'name'; + const sortByTime = sortBy == 'time'; + const sortIcon = sortOrder == 'asc' ? : ; - return items.length ? ( - <> - {table} - {this.props.isLoadingMore &&
} - - ) : emptyTip; - } + const isDesktop = Utils.isDesktop(); + // only for some columns + const columnWidths = isPro ? [0.14, 0.07, 0.14] : [0.21, 0.14, 0.2]; + + return ( + <> + {gettext('Name')} {sortByName && sortIcon}) }, + { 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: ({gettext('Expiration')} {sortByTime && sortIcon}) }, + { 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 (); + })} + + {this.props.isLoadingMore &&
} + + ); } } @@ -277,7 +270,7 @@ class Item extends Component { onMouseLeave={this.handleMouseLeave} onFocus={this.handleMouseEnter} > - + {item.is_dir ? {item.obj_name} : diff --git a/frontend/src/pages/share-admin/upload-links.js b/frontend/src/pages/share-admin/upload-links.js index 0f9b6105843..3e6adba32a6 100644 --- a/frontend/src/pages/share-admin/upload-links.js +++ b/frontend/src/pages/share-admin/upload-links.js @@ -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

{errorMsg}

; } - - const emptyTip = ( - - - ); + if (!items.length) { + return ( + + ); + } const isDesktop = Utils.isDesktop(); - const table = ( - - - {isDesktop ? ( - - - - - - - - - ) : ( - - - - - - )} - - - {items.map((item, index) => { - return (); - })} - -
{/* icon*/}{gettext('Name')}{gettext('Library')}{gettext('Visits')}{gettext('Expiration')}{/* Operations*/}
+ return ( + + {items.map((item, index) => { + return (); + })} + ); - - return items.length ? table : emptyTip; } } @@ -145,7 +139,7 @@ class Item extends Component { {this.props.isDesktop ? - + {item.obj_name} {item.obj_id === '' ? {gettext('(deleted)')} : null} diff --git a/frontend/src/tag/views/tag-files/index.js b/frontend/src/tag/views/tag-files/index.js index eacf2c2407d..47366853555 100644 --- a/frontend/src/tag/views/tag-files/index.js +++ b/frontend/src/tag/views/tag-files/index.js @@ -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 (); } const isSelectedAll = selectedFiles && selectedFiles.length === tagFiles.rows.length; + const headers = [ + { + isFixed: true, + width: 31, + className: 'pl10 pr-2', + children: ( + + ) + }, { + isFixed: true, + width: 41, + className: 'pl-2 pr-2', + }, { + isFixed: false, + width: 0.5, + children: ({gettext('Name')}), + }, { + isFixed: false, + width: 0.06, + }, { + isFixed: false, + width: 0.18, + }, { + isFixed: false, + width: 0.11, + children: ({gettext('Size')}), + }, { + isFixed: false, + width: 0.15, + children: ({gettext('Last Update')}), + } + ]; return ( <> -
- - - - - - - - - - - - - - {tagFiles.rows.map(file => { - const fileId = getRecordIdFromRecord(file); - return ( - ); - })} - -
- - {/* icon */}{gettext('Name')}{/* tag */}{/* operation */}{gettext('Size')}{gettext('Last Update')}
+
+ + {tagFiles.rows.map(file => { + const fileId = getRecordIdFromRecord(file); + return ( + ); + })} +
{isImagePreviewerVisible && (