diff --git a/frontend/src/components/dirent-detail/detail-item/index.css b/frontend/src/components/dirent-detail/detail-item/index.css new file mode 100644 index 00000000000..95e67b54b98 --- /dev/null +++ b/frontend/src/components/dirent-detail/detail-item/index.css @@ -0,0 +1,51 @@ +.dirent-detail-item { + width: 100%; + display: flex; + justify-content: space-between; + margin-bottom: 4px; + font-size: 14px; +} + +.dirent-detail-item .dirent-detail-item-name-container { + width: 160px; + padding: 7px 6px; + min-height: 34px; + height: fit-content; + color: #666; + font-size: 14px; + line-height: 1.4; +} + +.dirent-detail-item .dirent-detail-item-name-container .sf-metadata-icon { + margin-right: 6px; + font-size: 14px; + fill: #999; +} + +.dirent-detail-item .dirent-detail-item-value { + width: 200px; + display: flex; + padding: 7px 6px; + min-height: 34px; + height: fit-content; +} + +.dirent-detail-item .dirent-detail-item-value.editable:hover { + cursor: pointer; +} + +.dirent-detail-item .dirent-detail-item-name-container:hover, +.dirent-detail-item .dirent-detail-item-value:hover { + background-color: #F5F5F5; + border-radius: 3px; + cursor: default; +} + +.dirent-detail-item .dirent-detail-item-value .text-formatter, +.dirent-detail-item .dirent-detail-item-value .ctime-formatter { + line-height: 1.5; +} + +.dirent-detail-item-value .creator-formatter { + height: 20px; +} diff --git a/frontend/src/components/dirent-detail/detail-item/index.js b/frontend/src/components/dirent-detail/detail-item/index.js new file mode 100644 index 00000000000..de51d5c7c30 --- /dev/null +++ b/frontend/src/components/dirent-detail/detail-item/index.js @@ -0,0 +1,36 @@ +import React, { useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { Formatter, Icon } from '@seafile/sf-metadata-ui-component'; +import classnames from 'classnames'; +import { CellType, COLUMNS_ICON_CONFIG } from '../../../metadata/metadata-view/_basic'; + +import './index.css'; + +const DetailItem = ({ field, value, valueId, valueClick, children, ...params }) => { + const icon = useMemo(() => { + if (field.type === 'size') return COLUMNS_ICON_CONFIG[CellType.NUMBER]; + return COLUMNS_ICON_CONFIG[field.type]; + }, [field]); + + return ( +
+
+ + {field.name} +
+
+ {children ? children : ()} +
+
+ ); +}; + +DetailItem.propTypes = { + field: PropTypes.object.isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object]), + children: PropTypes.any, + valueId: PropTypes.string, +}; + +export default DetailItem; + diff --git a/frontend/src/components/dirent-detail/detail-list-view.js b/frontend/src/components/dirent-detail/detail-list-view.js index ce9616fd042..0a4d83e4d02 100644 --- a/frontend/src/components/dirent-detail/detail-list-view.js +++ b/frontend/src/components/dirent-detail/detail-list-view.js @@ -44,6 +44,14 @@ class DetailListView extends React.Component { return position; }; + getDirentPath = () => { + if (Utils.isMarkdownFile(this.props.path)) { + return this.props.path; // column mode: view file + } + let { dirent, path } = this.props; + return Utils.joinPath(path, dirent.name); + }; + onEditFileTagToggle = () => { this.setState({ isEditFileTagShow: !this.state.isEditFileTagShow @@ -55,14 +63,6 @@ class DetailListView extends React.Component { this.props.onFileTagChanged(this.props.dirent, direntPath); }; - getDirentPath = () => { - if (Utils.isMarkdownFile(this.props.path)) { - return this.props.path; // column mode: view file - } - let { dirent, path } = this.props; - return Utils.joinPath(path, dirent.name); - }; - toggleExtraMetadataPropertiesDialog = () => { this.setState({ isShowMetadataExtraProperties: !this.state.isShowMetadataExtraProperties }); }; diff --git a/frontend/src/components/dirent-detail/dirent-details.js b/frontend/src/components/dirent-detail/dirent-details.js deleted file mode 100644 index 128e67823de..00000000000 --- a/frontend/src/components/dirent-detail/dirent-details.js +++ /dev/null @@ -1,158 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { siteRoot, enableVideoThumbnail } from '../../utils/constants'; -import { seafileAPI } from '../../utils/seafile-api'; -import { Utils } from '../../utils/utils'; -import toaster from '../toast'; -import Dirent from '../../models/dirent'; -import DetailListView from './detail-list-view'; - -import '../../css/dirent-detail.css'; - -const propTypes = { - repoID: PropTypes.string.isRequired, - dirent: PropTypes.object, - path: PropTypes.string.isRequired, - currentRepoInfo: PropTypes.object.isRequired, - onItemDetailsClose: PropTypes.func.isRequired, - onFileTagChanged: PropTypes.func.isRequired, - direntDetailPanelTab: PropTypes.string, - repoTags: PropTypes.array, - fileTags: PropTypes.array, -}; - -class DirentDetail extends React.Component { - - constructor(props) { - super(props); - this.state = { - direntType: '', - direntDetail: '', - folderDirent: null, - }; - } - - componentDidMount() { - let { dirent, path, repoID } = this.props; - this.loadDirentInfo(dirent, path, repoID); - } - - UNSAFE_componentWillReceiveProps(nextProps) { - let { dirent, path, repoID } = nextProps; - if (this.props.dirent !== nextProps.dirent) { - this.loadDirentInfo(dirent, path, repoID); - } - } - - loadDirentInfo = (dirent, path, repoID) => { - if (dirent) { - let direntPath = Utils.joinPath(path, dirent.name); - this.updateDetailView(dirent, direntPath); - } else { - let dirPath = Utils.getDirName(path); - seafileAPI.listDir(repoID, dirPath).then(res => { - let direntList = res.data.dirent_list; - let folderDirent = null; - for (let i = 0; i < direntList.length; i++) { - let dirent = direntList[i]; - if (dirent.parent_dir + dirent.name === path) { - folderDirent = new Dirent(dirent); - break; - } - } - this.setState({ folderDirent: folderDirent }); - this.updateDetailView(folderDirent, path); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } - }; - - updateDetailView = (dirent, direntPath) => { - let repoID = this.props.repoID; - if (dirent.type === 'file') { - seafileAPI.getFileInfo(repoID, direntPath).then(res => { - this.setState({ - direntType: 'file', - direntDetail: res.data, - }); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } else { - seafileAPI.getDirInfo(repoID, direntPath).then(res => { - this.setState({ - direntType: 'dir', - direntDetail: res.data - }); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } - }; - - renderHeader = (smallIconUrl, direntName) => { - return ( -
-
-
- {' '} - {direntName} -
-
- ); - }; - - renderDetailBody = (bigIconUrl, folderDirent) => { - const { dirent, fileTags } = this.props; - return ( -
-
- {this.state.direntDetail && -
- -
- } -
- ); - }; - - render() { - let { dirent, repoID, path } = this.props; - let { folderDirent } = this.state; - if (!dirent && !folderDirent) { - return ''; - } - let smallIconUrl = dirent ? Utils.getDirentIcon(dirent) : Utils.getDirentIcon(folderDirent); - let bigIconUrl = dirent ? Utils.getDirentIcon(dirent, true) : Utils.getDirentIcon(folderDirent, true); - const isImg = dirent ? Utils.imageCheck(dirent.name) : Utils.imageCheck(folderDirent.name); - const isVideo = dirent ? Utils.videoCheck(dirent.name) : Utils.videoCheck(folderDirent.name); - if (isImg || (enableVideoThumbnail && isVideo)) { - bigIconUrl = `${siteRoot}thumbnail/${repoID}/1024` + Utils.encodePath(`${path === '/' ? '' : path}/${dirent.name}`); - } - let direntName = dirent ? dirent.name : folderDirent.name; - return ( -
- {this.renderHeader(smallIconUrl, direntName)} - {this.renderDetailBody(bigIconUrl, folderDirent)} -
- ); - } -} - -DirentDetail.propTypes = propTypes; - -export default DirentDetail; diff --git a/frontend/src/components/dirent-detail/dirent-details/dir-details.js b/frontend/src/components/dirent-detail/dirent-details/dir-details.js new file mode 100644 index 00000000000..36cb42e7b50 --- /dev/null +++ b/frontend/src/components/dirent-detail/dirent-details/dir-details.js @@ -0,0 +1,40 @@ +import React, { useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { getDirentPath, getDirentPosition } from './utils'; +import DetailItem from '../detail-item'; +import { CellType } from '../../../metadata/metadata-view/_basic'; +import { gettext } from '../../../utils/constants'; +import EditMetadata from './edit-metadata'; + +const DirDetails = ({ repoID, repoInfo, dirent, direntType, path, direntDetail }) => { + const position = useMemo(() => getDirentPosition(repoInfo, dirent, path), [repoInfo, dirent, path]); + const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]); + + return ( + <> + + + + + {direntDetail.permission === 'rw' && window.app.pageOptions.enableMetadataManagement && ( + + )} + + ); +}; + +DirDetails.propTypes = { + repoID: PropTypes.string, + repoInfo: PropTypes.object, + dirent: PropTypes.object, + direntType: PropTypes.string, + path: PropTypes.string, + direntDetail: PropTypes.object, +}; + +export default DirDetails; diff --git a/frontend/src/components/dirent-detail/dirent-details/edit-metadata/index.css b/frontend/src/components/dirent-detail/dirent-details/edit-metadata/index.css new file mode 100644 index 00000000000..6fc1a7a56c8 --- /dev/null +++ b/frontend/src/components/dirent-detail/dirent-details/edit-metadata/index.css @@ -0,0 +1,31 @@ +.detail-edit-metadata-btn { + height: 34px; + width: fit-content; + max-width: 100%; + padding: 0 6px; + display: flex; + align-items: center; + overflow: hidden; +} + +.detail-edit-metadata-btn .seafile-multicolor-icon { + margin-right: 6px; + flex-shrink: 0; + font-size: 14px; + fill: #999; +} + +.detail-edit-metadata-btn:hover { + background-color: #F5F5F5; + border-radius: 3px; + cursor: pointer; +} + +.detail-edit-metadata-btn .detail-edit-metadata-btn-title { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: #666; + font-size: 14px; +} diff --git a/frontend/src/components/dirent-detail/dirent-details/edit-metadata/index.js b/frontend/src/components/dirent-detail/dirent-details/edit-metadata/index.js new file mode 100644 index 00000000000..d393b5b2068 --- /dev/null +++ b/frontend/src/components/dirent-detail/dirent-details/edit-metadata/index.js @@ -0,0 +1,41 @@ +import React, { useCallback, useState } from 'react'; +import PropTypes from 'prop-types'; +import ExtraMetadataAttributesDialog from '../../../dialog/extra-metadata-attributes-dialog'; +import { gettext } from '../../../../utils/constants'; +import Icon from '../../../icon'; + +import './index.css'; + +const EditMetadata = ({ repoID, direntPath, direntType, direntDetail }) => { + const [isShowDialog, setShowDialog] = useState(false); + const onToggle = useCallback(() => { + setShowDialog(!isShowDialog); + }, [isShowDialog]); + + return ( + <> +
+ + {gettext('Edit metadata properties')} +
+ {isShowDialog && ( + + )} + + ); +}; + +EditMetadata.propTypes = { + repoID: PropTypes.string, + direntPath: PropTypes.string, + direntType: PropTypes.string, + direntDetail: PropTypes.object, +}; + +export default EditMetadata; diff --git a/frontend/src/components/dirent-detail/dirent-details/file-details.js b/frontend/src/components/dirent-detail/dirent-details/file-details.js new file mode 100644 index 00000000000..42f3199d189 --- /dev/null +++ b/frontend/src/components/dirent-detail/dirent-details/file-details.js @@ -0,0 +1,74 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import PropTypes from 'prop-types'; +import { v4 as uuidV4 } from 'uuid'; +import { getDirentPath, getDirentPosition } from './utils'; +import DetailItem from '../detail-item'; +import { CellType } from '../../../metadata/metadata-view/_basic'; +import { gettext } from '../../../utils/constants'; +import EditMetadata from './edit-metadata'; +import EditFileTagPopover from '../../popover/edit-filetag-popover'; +import FileTagList from '../../file-tag-list'; +import { Utils } from '../../../utils/utils'; + +const FileDetails = ({ repoID, repoInfo, dirent, direntType, path, direntDetail, onFileTagChanged, repoTags, fileTagList }) => { + const [isEditFileTagShow, setEditFileTagShow] = useState(false); + + const position = useMemo(() => getDirentPosition(repoInfo, dirent, path), [repoInfo, dirent, path]); + const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]); + const tagListTitleID = useMemo(() => `detail-list-view-tags-${uuidV4()}`, []); + + const onEditFileTagToggle = useCallback(() => { + setEditFileTagShow(!isEditFileTagShow); + }, [isEditFileTagShow]); + + const fileTagChanged = useCallback(() => { + onFileTagChanged(dirent, direntPath); + }, [dirent, direntPath, onFileTagChanged]); + + return ( + <> + + + + + + {Array.isArray(fileTagList) && fileTagList.length > 0 ? ( + + ) : ( + {gettext('Empty')} + )} + + {direntDetail.permission === 'rw' && window.app.pageOptions.enableMetadataManagement && ( + + )} + {isEditFileTagShow && + + } + + ); +}; + +FileDetails.propTypes = { + repoID: PropTypes.string, + repoInfo: PropTypes.object, + dirent: PropTypes.object, + direntType: PropTypes.string, + path: PropTypes.string, + direntDetail: PropTypes.object, + onFileTagChanged: PropTypes.func, +}; + +export default FileDetails; diff --git a/frontend/src/components/dirent-detail/dirent-details/index.css b/frontend/src/components/dirent-detail/dirent-details/index.css new file mode 100644 index 00000000000..05b93813d22 --- /dev/null +++ b/frontend/src/components/dirent-detail/dirent-details/index.css @@ -0,0 +1,29 @@ +.detail-container .detail-image-thumbnail { + height: 144px; + width: 100%; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 16px; + overflow: hidden; +} + +.detail-container .detail-image-thumbnail .thumbnail { + height: 100%; + width: 100%; + border: 0; + border-radius: 0; + float: none; + height: auto; + margin: 0; + padding: 0; + width: auto; + display: inline-block; + max-width: 100%; + max-height: 100%; +} + +.detail-container .empty-tip-text { + color: #666 +} diff --git a/frontend/src/components/dirent-detail/dirent-details/index.js b/frontend/src/components/dirent-detail/dirent-details/index.js new file mode 100644 index 00000000000..73cc879df5e --- /dev/null +++ b/frontend/src/components/dirent-detail/dirent-details/index.js @@ -0,0 +1,121 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import { siteRoot } from '../../../utils/constants'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { Utils } from '../../../utils/utils'; +import toaster from '../../toast'; +import Dirent from '../../../models/dirent'; +import Header from '../header'; +import DirDetails from './dir-details'; +import FileDetails from './file-details'; + +import './index.css'; + +const DirentDetails = ({ dirent, path, repoID, currentRepoInfo, repoTags, fileTags, onItemDetailsClose, onFileTagChanged }) => { + const [direntType, setDirentType] = useState(''); + const [direntDetail, setDirentDetail] = useState(''); + const [folderDirent, setFolderDirent] = useState(null); + const direntRef = useRef(null); + + const updateDetailView = useCallback((repoID, dirent, direntPath) => { + const apiName = dirent.type === 'file' ? 'getFileInfo' : 'getDirInfo'; + seafileAPI[apiName](repoID, direntPath).then(res => { + setDirentType(dirent.type === 'file' ? 'file' : 'dir'); + setDirentDetail(res.data); + }).catch(error => { + const errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + }, []); + + useEffect(() => { + if (direntRef.current && dirent === direntRef.current) return; + direntRef.current = dirent; + if (dirent) { + const direntPath = Utils.joinPath(path, dirent.name); + updateDetailView(repoID, dirent, direntPath); + return; + } + const dirPath = Utils.getDirName(path); + seafileAPI.listDir(repoID, dirPath).then(res => { + const direntList = res.data.dirent_list; + let folderDirent = null; + for (let i = 0; i < direntList.length; i++) { + let dirent = direntList[i]; + if (dirent.parent_dir + dirent.name === path) { + folderDirent = new Dirent(dirent); + break; + } + } + setFolderDirent(folderDirent); + updateDetailView(repoID, folderDirent, path); + }).catch(error => { + const errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dirent, path, repoID]); + + if (!dirent && !folderDirent) return ''; + const direntName = dirent ? dirent.name : folderDirent.name; + const smallIconUrl = dirent ? Utils.getDirentIcon(dirent) : Utils.getDirentIcon(folderDirent); + // let bigIconUrl = dirent ? Utils.getDirentIcon(dirent, true) : Utils.getDirentIcon(folderDirent, true); + let bigIconUrl = ''; + const isImg = dirent ? Utils.imageCheck(dirent.name) : Utils.imageCheck(folderDirent.name); + // const isVideo = dirent ? Utils.videoCheck(dirent.name) : Utils.videoCheck(folderDirent.name); + if (isImg) { + bigIconUrl = `${siteRoot}thumbnail/${repoID}/1024` + Utils.encodePath(`${path === '/' ? '' : path}/${dirent.name}`); + } + return ( +
+
+
+ {isImg && ( +
+ +
+ )} + {direntDetail && ( +
+ {direntType === 'dir' ? ( + + ) : ( + + )} +
+ )} +
+
+ ); +}; + +DirentDetails.propTypes = { + repoID: PropTypes.string.isRequired, + dirent: PropTypes.object, + path: PropTypes.string.isRequired, + currentRepoInfo: PropTypes.object.isRequired, + onItemDetailsClose: PropTypes.func.isRequired, + onFileTagChanged: PropTypes.func.isRequired, + direntDetailPanelTab: PropTypes.string, + repoTags: PropTypes.array, + fileTags: PropTypes.array, +}; + +export default DirentDetails; diff --git a/frontend/src/components/dirent-detail/dirent-details/utils.js b/frontend/src/components/dirent-detail/dirent-details/utils.js new file mode 100644 index 00000000000..0ca48f2f737 --- /dev/null +++ b/frontend/src/components/dirent-detail/dirent-details/utils.js @@ -0,0 +1,14 @@ +import { Utils } from '../../../utils/utils'; + +export const getDirentPath = (dirent, path) => { + if (Utils.isMarkdownFile(path)) return path; // column mode: view file + return Utils.joinPath(path, dirent.name); +}; + +export const getDirentPosition = (repoInfo, dirent, path) => { + const direntPath = getDirentPath(dirent, path); + const position = repoInfo.repo_name; + if (direntPath === '/') return position; + const index = direntPath.lastIndexOf('/'); + return position + direntPath.slice(0, index); +}; diff --git a/frontend/src/components/dirent-detail/header/index.css b/frontend/src/components/dirent-detail/header/index.css new file mode 100644 index 00000000000..df4ee6b249d --- /dev/null +++ b/frontend/src/components/dirent-detail/header/index.css @@ -0,0 +1,45 @@ +.detail-header { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + line-height: 2.5rem; + background-color: #f9f9f9; + border-bottom: 1px solid #e8e8e8; + height: 48px; + padding: 8px 16px; +} + +.detail-header .detail-title { + display: flex; + flex: 1; + align-items: center; + width: 0; /* prevent strut flex layout */ +} + +.detail-header .detail-title .name { + margin: 0 0.5rem 0 6px; + line-height: 1.5rem; + vertical-align: middle; + font-size: 1rem; + color: #212529; +} + +.detail-header .detail-control { + height: 24px; + width: 24px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.detail-header .detail-control .detail-control-close { + font-size: 16px; + fill: #666; +} + +.detail-header .detail-control:hover { + background-color: #EFEFEF; + border-radius: 3px; +} diff --git a/frontend/src/components/dirent-detail/header/index.js b/frontend/src/components/dirent-detail/header/index.js new file mode 100644 index 00000000000..83a2da240e2 --- /dev/null +++ b/frontend/src/components/dirent-detail/header/index.js @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Icon from '../../icon'; + +import './index.css'; + +const Header = ({ title, icon, onClose }) => { + + return ( +
+
+ + {title} +
+
+ +
+
+ ); +}; + +Header.propTypes = { + title: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, +}; + +export default Header; diff --git a/frontend/src/components/dirent-detail/index.css b/frontend/src/components/dirent-detail/index.css new file mode 100644 index 00000000000..02e422ebaae --- /dev/null +++ b/frontend/src/components/dirent-detail/index.css @@ -0,0 +1,33 @@ +.detail-container { + flex: 1; + display: flex; + flex-direction: column; +} + +.detail-container .detail-content { + display: flex; + flex-direction: column; +} + +.detail-body { + flex: 1; + display: flex; + flex-direction: column; + overflow-y: auto; + overflow-x: hidden; + padding: 16px; +} + +.dirent-info .img { + height: 10rem; + padding: 0.5rem 0; + display: flex; + justify-content: center; + align-items: center; +} + +.dirent-info .img .thumbnail { + max-width: calc(100% - 4px); + max-height: 100%; + display: inline-block; +} diff --git a/frontend/src/components/dirent-detail/index.js b/frontend/src/components/dirent-detail/index.js new file mode 100644 index 00000000000..63dd0640264 --- /dev/null +++ b/frontend/src/components/dirent-detail/index.js @@ -0,0 +1,9 @@ +import LibDetail from './lib-details'; +import DirentDetail from './dirent-details'; + +import './index.css'; + +export { + LibDetail, + DirentDetail, +}; diff --git a/frontend/src/components/dirent-detail/lib-details.js b/frontend/src/components/dirent-detail/lib-details.js deleted file mode 100644 index d5ecaa0c2c7..00000000000 --- a/frontend/src/components/dirent-detail/lib-details.js +++ /dev/null @@ -1,82 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import moment from 'moment'; -import { Utils } from '../../utils/utils'; -import { gettext } from '../../utils/constants'; -import { seafileAPI } from '../../utils/seafile-api'; -import toaster from '../toast'; -import '../../css/dirent-detail.css'; - -const propTypes = { - currentRepo: PropTypes.object.isRequired, - closeDetails: PropTypes.func.isRequired, -}; - -class LibDetail extends React.Component { - - constructor(props) { - super(props); - this.state = { - fileCount: 0, - }; - } - - componentDidMount() { - let repo = this.props.currentRepo; - this.getFileCounts(repo); - } - - UNSAFE_componentWillReceiveProps(nextProps) { - if (nextProps.currentRepo.repo_id !== this.props.currentRepo.repo_id) { - this.getFileCounts(nextProps.currentRepo); - } - } - - getFileCounts = (repo) => { - seafileAPI.getRepoInfo(repo.repo_id).then(res => { - this.setState({ fileCount: res.data.file_count }); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - }; - - render() { - let repo = this.props.currentRepo; - let smallIconUrl = Utils.getLibIconUrl(repo); - let bigIconUrl = Utils.getLibIconUrl(repo, true); - - return ( -
-
-
-
- {' '} - {repo.repo_name} -
-
-
-
- -
-
- - - - - - - - - -
{gettext('Files')}{this.state.fileCount}
{gettext('Size')}{repo.size}
{gettext('Last Update')}{ moment(repo.last_modified).fromNow()}
-
-
-
- ); - } -} - -LibDetail.propTypes = propTypes; - -export default LibDetail; diff --git a/frontend/src/components/dirent-detail/lib-details/index.js b/frontend/src/components/dirent-detail/lib-details/index.js new file mode 100644 index 00000000000..5236507c24e --- /dev/null +++ b/frontend/src/components/dirent-detail/lib-details/index.js @@ -0,0 +1,62 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import PropTypes from 'prop-types'; +import { Utils } from '../../../utils/utils'; +import { gettext } from '../../../utils/constants'; +import { seafileAPI } from '../../../utils/seafile-api'; +import toaster from '../../toast'; +import Header from '../header'; +import Repo from '../../../models/repo'; +import Loading from '../../loading'; +import DetailItem from '../detail-item'; +import { CellType } from '../../../metadata/metadata-view/_basic'; + +const LibDetail = React.memo(({ currentRepo, closeDetails }) => { + const [isLoading, setLoading] = useState(true); + const [repo, setRepo] = useState({}); + const smallIconUrl = useMemo(() => Utils.getLibIconUrl(currentRepo), [currentRepo]); + + useEffect(() => { + setLoading(true); + seafileAPI.getRepoInfo(currentRepo.repo_id).then(res => { + const repo = new Repo(res.data); + setRepo(repo); + setLoading(false); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + }, [currentRepo.repo_id]); + + return ( +
+
+
+ {isLoading ? ( +
+ ) : ( +
+ + + + +
+ )} +
+
+ ); + +}, (props, nextProps) => { + return props.currentRepo.repo_id === nextProps.currentRepo.repo_id; +}); + +LibDetail.propTypes = { + currentRepo: PropTypes.object.isRequired, + closeDetails: PropTypes.func.isRequired, +}; + +export default LibDetail; diff --git a/frontend/src/css/dirent-detail.css b/frontend/src/css/dirent-detail.css index 3e2ce0f9dd4..fc0df0b81a0 100644 --- a/frontend/src/css/dirent-detail.css +++ b/frontend/src/css/dirent-detail.css @@ -2,45 +2,6 @@ flex: 1; display: flex; flex-direction: column; - border-left: 1px solid #e8e8e8; -} - -.detail-header { - position: relative; - display: flex; - align-items: center; - justify-content: center; - line-height: 2.5rem; - background-color: #f9f9f9; - border-bottom: 1px solid #e8e8e8; - height: 40px; -} - -.detail-header .detail-control { - padding-left: 0.5rem; - font-size: 16px; - color: #b9b9b9; -} - -.detail-header .detail-control:hover { - color: #888; -} - -.detail-header .detail-title { - margin-left: 0.25rem; - display: flex; - flex: 1; - justify-content: center; - align-items: center; - width: 0; /* prevent strut flex layout */ -} - -.detail-header .detail-title .name { - margin: 0 0.5rem 0 0.25rem; - line-height: 1.5rem; - vertical-align: middle; - font-size: 1rem; - color: #212529; } .detail-body { diff --git a/frontend/src/css/layout.css b/frontend/src/css/layout.css index 713d4a6dec4..0c8c411b11e 100644 --- a/frontend/src/css/layout.css +++ b/frontend/src/css/layout.css @@ -183,7 +183,7 @@ position: absolute; right: 0; background-color: #fff; - width: 300px; + width: 400px; height: 100%; box-shadow: -1px 0 3px 0 #ccc; animation: move .5s ease-in-out 1; diff --git a/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/group-container-right.js b/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/group-container-right.js index d2c78998e02..aa39aa64866 100644 --- a/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/group-container-right.js +++ b/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/group-container-right.js @@ -33,7 +33,6 @@ class GroupContainerRight extends Component { isExpanded={isExpanded} columns={columns} summaryConfigs={summaryConfigs} - getTableContentRect={this.props.getTableContentRect} /> ); @@ -50,7 +49,6 @@ GroupContainerRight.propTypes = { height: PropTypes.number, groupOffsetLeft: PropTypes.number, lastFrozenColumnKey: PropTypes.string, - getTableContentRect: PropTypes.func, }; export default GroupContainerRight; diff --git a/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/group-header-cell.js b/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/group-header-cell.js index 080d8991fc9..16773dfa28b 100644 --- a/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/group-header-cell.js +++ b/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/group-header-cell.js @@ -10,9 +10,8 @@ class GroupHeaderCell extends React.PureComponent { fixedFrozenDOMs = (scrollLeft, scrollTop) => { if (this.headerCell) { const { firstColumnWidth, groupOffsetLeft } = this.props; - const tableContentLeft = this.props.getTableContentRect(); this.headerCell.style.position = 'fixed'; - this.headerCell.style.marginLeft = (SEQUENCE_COLUMN_WIDTH + firstColumnWidth + groupOffsetLeft + tableContentLeft) + 'px'; + this.headerCell.style.marginLeft = (SEQUENCE_COLUMN_WIDTH + firstColumnWidth + groupOffsetLeft) + 'px'; this.headerCell.style.marginTop = (-scrollTop) + 'px'; } }; @@ -64,7 +63,6 @@ GroupHeaderCell.propTypes = { groupOffsetLeft: PropTypes.number, summary: PropTypes.object, summaryMethod: PropTypes.string, - getTableContentRect: PropTypes.func, }; export default GroupHeaderCell; diff --git a/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/group-header-right.js b/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/group-header-right.js index b300b8242d8..de71c75f949 100644 --- a/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/group-header-right.js +++ b/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/group-header-right.js @@ -57,7 +57,6 @@ class GroupHeaderRight extends Component { isExpanded={isExpanded} summary={summary} summaryMethod={summaryMethod} - getTableContentRect={this.props.getTableContentRect} /> ); }); @@ -79,7 +78,6 @@ GroupHeaderRight.propTypes = { lastFrozenColumnKey: PropTypes.string, columns: PropTypes.array, summaryConfigs: PropTypes.object, - getTableContentRect: PropTypes.func, }; export default GroupHeaderRight; diff --git a/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/index.js b/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/index.js index 0d7736a3a38..d728656d23b 100644 --- a/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/index.js +++ b/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/index.js @@ -133,7 +133,6 @@ class GroupContainer extends Component { lastFrozenColumnKey={lastFrozenColumnKey} columns={columns} summaryConfigs={summaryConfigs} - getTableContentRect={this.props.getTableContentRect} /> ); @@ -157,7 +156,6 @@ GroupContainer.propTypes = { scrollLeft: PropTypes.number, maxLevel: PropTypes.number, summaryConfigs: PropTypes.object, - getTableContentRect: PropTypes.func, onExpandGroupToggle: PropTypes.func, updateSummaryConfig: PropTypes.func, }; diff --git a/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/index.js b/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/index.js index 78425cfa66b..2b394c48e18 100644 --- a/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/index.js +++ b/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/index.js @@ -790,7 +790,6 @@ class GroupBody extends Component { isExpanded={isExpanded} folding={folding} lastFrozenColumnKey={lastFrozenColumnKey} - getTableContentRect={this.props.getTableContentRect} onExpandGroupToggle={this.onExpandGroupToggle} /> ); diff --git a/frontend/src/metadata/metadata-view/components/table/table-main/records/record-footer/index.js b/frontend/src/metadata/metadata-view/components/table/table-main/records/record-footer/index.js index 7d717e84d42..b8ab4daa189 100644 --- a/frontend/src/metadata/metadata-view/components/table/table-main/records/record-footer/index.js +++ b/frontend/src/metadata/metadata-view/components/table/table-main/records/record-footer/index.js @@ -136,7 +136,7 @@ class RecordsFooter extends React.Component { const recordWidth = (isLoadingMore || hasMore ? SEQUENCE_COLUMN_WIDTH + columns[0].width : SEQUENCE_COLUMN_WIDTH) + groupOffsetLeft; return ( -
this.ref = ref}> +
this.ref = ref}>
{this.getRecord()} {!isLoadingMore && hasMore && diff --git a/frontend/src/metadata/metadata-view/components/view-toolbar/index.js b/frontend/src/metadata/metadata-view/components/view-toolbar/index.js index 5a12146bdc0..cd5c12d3523 100644 --- a/frontend/src/metadata/metadata-view/components/view-toolbar/index.js +++ b/frontend/src/metadata/metadata-view/components/view-toolbar/index.js @@ -65,8 +65,7 @@ const ViewToolBar = ({ metadataViewId }) => { return (
diff --git a/frontend/src/models/repo-info.js b/frontend/src/models/repo-info.js index 66632250466..0172879da27 100644 --- a/frontend/src/models/repo-info.js +++ b/frontend/src/models/repo-info.js @@ -10,6 +10,8 @@ class RepoInfo { this.owner_name = object.owner_name; this.owner_email = object.owner_email; this.owner_contact_email = object.owner_contact_email; + this.owner_avatar = object.owner_avatar || ''; + // is repo shared admin; // is repo shared admin && is one of current ordinary group's admins; // is one of current group owned group's admins; diff --git a/frontend/src/models/repo.js b/frontend/src/models/repo.js index 744c36f1cbf..bc26e466d85 100644 --- a/frontend/src/models/repo.js +++ b/frontend/src/models/repo.js @@ -4,17 +4,26 @@ class Repo { constructor(object) { this.repo_id = object.repo_id; this.repo_name = object.repo_name; + this.repo_type = object.repo_type; this.permission = object.permission; this.size_original = object.size; this.size = Utils.bytesToSize(object.size); + + // owner info this.owner_name = object.owner_name; this.owner_email = object.owner_email; this.owner_contact_email = object.owner_contact_email; + this.owner_avatar = object.owner_avatar || ''; + this.encrypted = object.encrypted; + + // last_modified: last modified time this.last_modified = object.last_modified; this.modifier_contact_email = object.modifier_contact_email; this.modifier_email = object.modifier_email; this.modifier_name = object.modifier_name; + this.modifier_avatar = object.modifier_avatar; + this.type = object.type; this.starred = object.starred; this.monitored = object.monitored; @@ -23,6 +32,11 @@ class Repo { if (object.is_admin != undefined) { this.is_admin = object.is_admin; } + this.file_count = object.file_count || 0; + this.has_been_shared_out = object.has_been_shared_out; + this.is_virtual = object.is_virtual; + this.lib_need_decrypt = object.lib_need_decrypt; + this.no_quota = object.no_quota; } } diff --git a/frontend/src/pages/lib-content-view/lib-content-container.js b/frontend/src/pages/lib-content-view/lib-content-container.js index 2bcfbf3d3dc..112447b782c 100644 --- a/frontend/src/pages/lib-content-view/lib-content-container.js +++ b/frontend/src/pages/lib-content-view/lib-content-container.js @@ -2,8 +2,7 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { gettext } from '../../utils/constants'; import CurDirPath from '../../components/cur-dir-path'; -import DirentDetail from '../../components/dirent-detail/dirent-details'; -import LibDetail from '../../components/dirent-detail/lib-details'; +import { LibDetail, DirentDetail } from '../../components/dirent-detail'; import DirColumnView from '../../components/dir-view-mode/dir-column-view'; import ToolbarForSelectedDirents from '../../components/toolbar/selected-dirents-toolbar'; @@ -318,31 +317,29 @@ class LibContentContainer extends React.Component { isDirentDetailShow={this.props.isDirentDetailShow} /> )} + {this.props.isDirentDetailShow && ( +
+ {(this.props.path === '/' && !this.state.currentDirent) ? + : + + } +
+ )}
- {this.props.isDirentDetailShow && - -
- {(this.props.path === '/' && !this.state.currentDirent) ? - : - - } -
-
- } ); } diff --git a/frontend/src/pages/org-admin/org-info.js b/frontend/src/pages/org-admin/org-info.js index 5dbf01dcace..d4081b779bc 100644 --- a/frontend/src/pages/org-admin/org-info.js +++ b/frontend/src/pages/org-admin/org-info.js @@ -126,7 +126,7 @@ class OrgInfo extends Component { {Utils.bytesToSize(user_default_quota)} diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css index 9e64c93a640..668fc1c598e 100644 --- a/media/css/seahub_react.css +++ b/media/css/seahub_react.css @@ -612,8 +612,8 @@ a, a:hover { color: #ec8000; } .side-nav { flex:auto; display:flex; - flex-direction:column; - justify-content:space-between; /* make .side-nav-footer on the bottom */ + flex-direction: column; + justify-content: space-between; /* make .side-nav-footer on the bottom */ overflow:hidden; /* for ff */ } diff --git a/seahub/api2/endpoints/repos.py b/seahub/api2/endpoints/repos.py index 830843496a0..7c2cb6a4097 100644 --- a/seahub/api2/endpoints/repos.py +++ b/seahub/api2/endpoints/repos.py @@ -25,6 +25,7 @@ from seahub.utils.timeutils import timestamp_to_isoformat_timestr from seahub.utils.repo import get_repo_owner, is_repo_admin, \ repo_has_been_shared_out, normalize_repo_status_code +from seahub.avatar.templatetags.avatar_tags import api_avatar_url from seahub.settings import ENABLE_STORAGE_CLASSES @@ -116,7 +117,7 @@ def get(self, request): if is_wiki_repo(r): continue - + url, _, _ = api_avatar_url(email, int(24)) repo_info = { "type": "mine", @@ -125,6 +126,7 @@ def get(self, request): "owner_email": email, "owner_name": email2nickname(email), "owner_contact_email": email2contact_email(email), + "owner_avatar": url, "last_modified": timestamp_to_isoformat_timestr(r.last_modify), "modifier_email": r.last_modifier, "modifier_name": nickname_dict.get(r.last_modifier, ''), @@ -189,6 +191,7 @@ def get(self, request): owner_name = group_name if is_group_owned_repo else nickname_dict.get(owner_email, '') owner_contact_email = '' if is_group_owned_repo else contact_email_dict.get(owner_email, '') + url, _, _ = api_avatar_url(owner_email, int(24)) repo_info = { "type": "shared", @@ -201,6 +204,7 @@ def get(self, request): "owner_email": owner_email, "owner_name": owner_name, "owner_contact_email": owner_contact_email, + "owner_avatar": url, "size": r.size, "encrypted": r.encrypted, "permission": r.permission, @@ -299,6 +303,7 @@ def get(self, request): continue repo_owner = repo_id_owner_dict[r.repo_id] + url, _, _ = api_avatar_url(repo_owner, int(24)) repo_info = { "type": "public", "repo_id": r.repo_id, @@ -310,6 +315,7 @@ def get(self, request): "owner_email": repo_owner, "owner_name": nickname_dict.get(repo_owner, ''), "owner_contact_email": contact_email_dict.get(repo_owner, ''), + "owner_avatar": url, "size": r.size, "encrypted": r.encrypted, "permission": r.permission, @@ -363,13 +369,14 @@ def get(self, request, repo_id): lib_need_decrypt = True repo_owner = get_repo_owner(request, repo_id) + url, _, _ = api_avatar_url(repo_owner, int(24)) try: has_been_shared_out = repo_has_been_shared_out(request, repo_id) except Exception as e: has_been_shared_out = False logger.error(e) - + result = { "repo_id": repo.id, "repo_name": repo.name, @@ -377,6 +384,7 @@ def get(self, request, repo_id): "owner_email": repo_owner, "owner_name": email2nickname(repo_owner), "owner_contact_email": email2contact_email(repo_owner), + "owner_avatar": url, "size": repo.size, "encrypted": repo.encrypted, diff --git a/seahub/api2/views.py b/seahub/api2/views.py index e4e4818eccc..c566349723a 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -3390,6 +3390,9 @@ def get(self, request, repo_id, format=None): entry["last_modifier_email"] = latest_contributor entry["last_modifier_name"] = email2nickname(latest_contributor) entry["last_modifier_contact_email"] = email2contact_email(latest_contributor) + if latest_contributor: + url, _, _ = api_avatar_url(latest_contributor, int(24)) + entry["last_modifier_avatar"] = url try: file_size = get_file_size(real_repo_id, repo.version, obj_id)