1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-04-28 03:10:45 +00:00

feat: details (#6484)

Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
杨国璇 2024-08-02 22:31:46 +08:00 committed by GitHub
parent 5c2f05ee8d
commit 6ed1fe58bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 680 additions and 331 deletions

View File

@ -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;
}

View File

@ -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 (
<div className="dirent-detail-item">
<div className="dirent-detail-item-name-container">
<Icon iconName={icon} />
<span className="dirent-detail-item-name">{field.name}</span>
</div>
<div className={classnames('dirent-detail-item-value', { 'editable': valueClick })} id={valueId} onClick={valueClick}>
{children ? children : (<Formatter { ...params } field={field} value={value}/>)}
</div>
</div>
);
};
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;

View File

@ -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 });
};

View File

@ -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 (
<div className="detail-header">
<div className="detail-control sf2-icon-x1" onClick={this.props.onItemDetailsClose}></div>
<div className="detail-title dirent-title">
<img src={smallIconUrl} width="24" height="24" alt="" />{' '}
<span className="name ellipsis" title={direntName}>{direntName}</span>
</div>
</div>
);
};
renderDetailBody = (bigIconUrl, folderDirent) => {
const { dirent, fileTags } = this.props;
return (
<div className="detail-body dirent-info">
<div className="img"><img src={bigIconUrl} className="thumbnail" alt="" /></div>
{this.state.direntDetail &&
<div className="dirent-table-container">
<DetailListView
repoInfo={this.props.currentRepoInfo}
path={this.props.path}
repoID={this.props.repoID}
dirent={this.props.dirent || folderDirent}
direntType={this.state.direntType}
direntDetail={this.state.direntDetail}
repoTags={this.props.repoTags}
fileTagList={dirent ? dirent.file_tags : fileTags}
onFileTagChanged={this.props.onFileTagChanged}
/>
</div>
}
</div>
);
};
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 (
<div className="detail-container">
{this.renderHeader(smallIconUrl, direntName)}
{this.renderDetailBody(bigIconUrl, folderDirent)}
</div>
);
}
}
DirentDetail.propTypes = propTypes;
export default DirentDetail;

View File

@ -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 (
<>
<DetailItem field={{ type: CellType.TEXT, name: gettext('File location') }} value={position} />
<DetailItem field={{ type: 'size', name: gettext('Size') }} value={repoInfo.size} />
<DetailItem field={{ type: CellType.CREATOR, name: gettext('Creator') }} value={repoInfo.owner_email} collaborators={[{
name: repoInfo.owner_name,
contact_email: repoInfo.owner_contact_email,
email: repoInfo.owner_email,
avatar_url: repoInfo.owner_avatar,
}]} />
<DetailItem field={{ type: CellType.MTIME, name: gettext('Last modified time') }} value={direntDetail.mtime} />
{direntDetail.permission === 'rw' && window.app.pageOptions.enableMetadataManagement && (
<EditMetadata repoID={repoID} direntPath={direntPath} direntType={direntType} direntDetail={direntDetail} />
)}
</>
);
};
DirDetails.propTypes = {
repoID: PropTypes.string,
repoInfo: PropTypes.object,
dirent: PropTypes.object,
direntType: PropTypes.string,
path: PropTypes.string,
direntDetail: PropTypes.object,
};
export default DirDetails;

View File

@ -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;
}

View File

@ -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 (
<>
<div className="detail-edit-metadata-btn" onClick={onToggle}>
<Icon symbol="add-table" />
<span className="detail-edit-metadata-btn-title">{gettext('Edit metadata properties')}</span>
</div>
{isShowDialog && (
<ExtraMetadataAttributesDialog
repoID={repoID}
filePath={direntPath}
direntType={direntType}
direntDetail={direntDetail}
onToggle={onToggle}
/>
)}
</>
);
};
EditMetadata.propTypes = {
repoID: PropTypes.string,
direntPath: PropTypes.string,
direntType: PropTypes.string,
direntDetail: PropTypes.object,
};
export default EditMetadata;

View File

@ -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 (
<>
<DetailItem field={{ type: CellType.TEXT, name: gettext('File location') }} value={position} />
<DetailItem field={{ type: 'size', name: gettext('Size') }} value={Utils.bytesToSize(direntDetail.size)} />
<DetailItem field={{ type: CellType.CREATOR, name: gettext('Creator') }} value={direntDetail.last_modifier_email} collaborators={[{
name: direntDetail.last_modifier_name,
contact_email: direntDetail.last_modifier_contact_email,
email: direntDetail.last_modifier_email,
avatar_url: direntDetail.last_modifier_avatar,
}]} />
<DetailItem field={{ type: CellType.MTIME, name: gettext('Last modified time') }} value={direntDetail.last_modified} />
<DetailItem field={{ type: CellType.SINGLE_SELECT, name: gettext('Tags') }} valueId={tagListTitleID} valueClick={onEditFileTagToggle} >
{Array.isArray(fileTagList) && fileTagList.length > 0 ? (
<FileTagList fileTagList={fileTagList} />
) : (
<span className="empty-tip-text">{gettext('Empty')}</span>
)}
</DetailItem>
{direntDetail.permission === 'rw' && window.app.pageOptions.enableMetadataManagement && (
<EditMetadata repoID={repoID} direntPath={direntPath} direntType={direntType} direntDetail={direntDetail} />
)}
{isEditFileTagShow &&
<EditFileTagPopover
repoID={repoID}
repoTags={repoTags}
filePath={direntPath}
fileTagList={fileTagList}
toggleCancel={onEditFileTagToggle}
onFileTagChanged={fileTagChanged}
target={tagListTitleID}
/>
}
</>
);
};
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;

View File

@ -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
}

View File

@ -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 (
<div className="detail-container">
<Header title={direntName} icon={smallIconUrl} onClose={onItemDetailsClose} />
<div className="detail-body dirent-info">
{isImg && (
<div className="detail-image-thumbnail">
<img src={bigIconUrl} alt="" className="thumbnail" />
</div>
)}
{direntDetail && (
<div className="detail-content">
{direntType === 'dir' ? (
<DirDetails
repoID={repoID}
repoInfo={currentRepoInfo}
dirent={dirent || folderDirent}
direntType={direntType}
direntDetail={direntDetail}
path={path}
/>
) : (
<FileDetails
repoID={repoID}
repoInfo={currentRepoInfo}
dirent={dirent || folderDirent}
direntType={direntType}
path={path}
direntDetail={direntDetail}
repoTags={repoTags}
fileTagList={dirent ? dirent.file_tags : fileTags}
onFileTagChanged={onFileTagChanged}
/>
)}
</div>
)}
</div>
</div>
);
};
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;

View File

@ -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);
};

View File

@ -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;
}

View File

@ -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 (
<div className="detail-header">
<div className="detail-title dirent-title">
<img src={icon} width="32" height="32" alt="" />
<span className="name ellipsis" title={title}>{title}</span>
</div>
<div className="detail-control" onClick={onClose}>
<Icon symbol="close" className="detail-control-close" />
</div>
</div>
);
};
Header.propTypes = {
title: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
};
export default Header;

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
import LibDetail from './lib-details';
import DirentDetail from './dirent-details';
import './index.css';
export {
LibDetail,
DirentDetail,
};

View File

@ -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 (
<div className="detail-container">
<div className="detail-header">
<div className="detail-control sf2-icon-x1" onClick={this.props.closeDetails}></div>
<div className="detail-title dirent-title">
<img src={smallIconUrl} width="24" height="24" alt="" />{' '}
<span className="name ellipsis" title={repo.repo_name}>{repo.repo_name}</span>
</div>
</div>
<div className="detail-body dirent-info">
<div className="img">
<img src={bigIconUrl} height="96" alt="" />
</div>
<div className="dirent-table-container">
<table className="table-thead-hidden">
<thead>
<tr><th width="35%"></th><th width="65%"></th></tr>
</thead>
<tbody>
<tr><th>{gettext('Files')}</th><td>{this.state.fileCount}</td></tr>
<tr><th>{gettext('Size')}</th><td>{repo.size}</td></tr>
<tr><th>{gettext('Last Update')}</th><td>{ moment(repo.last_modified).fromNow()}</td></tr>
</tbody>
</table>
</div>
</div>
</div>
);
}
}
LibDetail.propTypes = propTypes;
export default LibDetail;

View File

@ -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 (
<div className="detail-container">
<Header title={currentRepo.repo_name} icon={smallIconUrl} onClose={closeDetails} />
<div className="detail-body dirent-info">
{isLoading ? (
<div className="w-100 h-100 d-flex algin-items-center justify-content-center"><Loading /></div>
) : (
<div className="detail-content">
<DetailItem field={{ type: CellType.NUMBER, name: gettext('Files') }} value={repo.file_count} />
<DetailItem field={{ type: 'size', name: gettext('Size') }} value={repo.size} />
<DetailItem field={{ type: CellType.CREATOR, name: gettext('Creator') }} value={repo.owner_email} collaborators={[{
name: repo.owner_name,
contact_email: repo.owner_contact_email,
email: repo.owner_email,
avatar_url: repo.owner_avatar,
}]} />
<DetailItem field={{ type: CellType.MTIME, name: gettext('Last modified time') }} value={repo.last_modified} />
</div>
)}
</div>
</div>
);
}, (props, nextProps) => {
return props.currentRepo.repo_id === nextProps.currentRepo.repo_id;
});
LibDetail.propTypes = {
currentRepo: PropTypes.object.isRequired,
closeDetails: PropTypes.func.isRequired,
};
export default LibDetail;

View File

@ -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 {

View File

@ -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;

View File

@ -33,7 +33,6 @@ class GroupContainerRight extends Component {
isExpanded={isExpanded}
columns={columns}
summaryConfigs={summaryConfigs}
getTableContentRect={this.props.getTableContentRect}
/>
</div>
);
@ -50,7 +49,6 @@ GroupContainerRight.propTypes = {
height: PropTypes.number,
groupOffsetLeft: PropTypes.number,
lastFrozenColumnKey: PropTypes.string,
getTableContentRect: PropTypes.func,
};
export default GroupContainerRight;

View File

@ -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;

View File

@ -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;

View File

@ -133,7 +133,6 @@ class GroupContainer extends Component {
lastFrozenColumnKey={lastFrozenColumnKey}
columns={columns}
summaryConfigs={summaryConfigs}
getTableContentRect={this.props.getTableContentRect}
/>
</div>
);
@ -157,7 +156,6 @@ GroupContainer.propTypes = {
scrollLeft: PropTypes.number,
maxLevel: PropTypes.number,
summaryConfigs: PropTypes.object,
getTableContentRect: PropTypes.func,
onExpandGroupToggle: PropTypes.func,
updateSummaryConfig: PropTypes.func,
};

View File

@ -790,7 +790,6 @@ class GroupBody extends Component {
isExpanded={isExpanded}
folding={folding}
lastFrozenColumnKey={lastFrozenColumnKey}
getTableContentRect={this.props.getTableContentRect}
onExpandGroupToggle={this.onExpandGroupToggle}
/>
);

View File

@ -136,7 +136,7 @@ class RecordsFooter extends React.Component {
const recordWidth = (isLoadingMore || hasMore ? SEQUENCE_COLUMN_WIDTH + columns[0].width : SEQUENCE_COLUMN_WIDTH) + groupOffsetLeft;
return (
<div className="sf-metadata-result-footer" style={{ zIndex: Z_INDEX.GRID_FOOTER, transform: 'translateZ(1000px)' }} ref={ref => this.ref = ref}>
<div className="sf-metadata-result-footer" style={{ zIndex: Z_INDEX.GRID_FOOTER }} ref={ref => this.ref = ref}>
<div className="rows-record d-flex text-nowrap" style={{ width: recordWidth }}>
<span>{this.getRecord()}</span>
{!isLoadingMore && hasMore &&

View File

@ -65,8 +65,7 @@ const ViewToolBar = ({ metadataViewId }) => {
return (
<div
className='sf-metadata-tool'
// style={{ zIndex: Z_INDEX.TABLE_HEADER, transform: 'translateZ(1000px)' }}
className="sf-metadata-tool"
onClick={onHeaderClick}
>
<div className="sf-metadata-tool-left-operations">

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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 && (
<div className="cur-view-detail">
{(this.props.path === '/' && !this.state.currentDirent) ?
<LibDetail
currentRepo={this.props.currentRepoInfo}
closeDetails={this.props.closeDirentDetail}
/> :
<DirentDetail
repoID={repoID}
path={this.props.path}
dirent={this.state.currentDirent}
currentRepoInfo={this.props.currentRepoInfo}
repoTags={this.props.repoTags}
fileTags={this.props.isViewFile ? this.props.fileTags : []}
onFileTagChanged={this.props.onFileTagChanged}
onItemDetailsClose={this.props.closeDirentDetail}
direntDetailPanelTab={this.props.direntDetailPanelTab}
/>
}
</div>
)}
</div>
</div>
{this.props.isDirentDetailShow &&
<Fragment>
<div className="cur-view-detail">
{(this.props.path === '/' && !this.state.currentDirent) ?
<LibDetail
currentRepo={this.props.currentRepoInfo}
closeDetails={this.props.closeDirentDetail}
/> :
<DirentDetail
repoID={repoID}
path={this.props.path}
dirent={this.state.currentDirent}
currentRepoInfo={this.props.currentRepoInfo}
repoTags={this.props.repoTags}
fileTags={this.props.isViewFile ? this.props.fileTags : []}
onFileTagChanged={this.props.onFileTagChanged}
onItemDetailsClose={this.props.closeDirentDetail}
direntDetailPanelTab={this.props.direntDetailPanelTab}
/>
}
</div>
</Fragment>
}
</Fragment>
);
}

View File

@ -126,7 +126,7 @@ class OrgInfo extends Component {
<span>{Utils.bytesToSize(user_default_quota)}</span>
<span
title={gettext('Edit')}
className={`sf3-font sf3-font-rename attr-action-icon`}
className="sf3-font sf3-font-rename attr-action-icon"
onClick={this.toggleSetUserDefaultQuotaDialog}>
</span>

View File

@ -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 */
}

View File

@ -25,6 +25,7 @@ from seahub.utils import is_org_context, is_pro_version
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 @@ class ReposView(APIView):
if is_wiki_repo(r):
continue
url, _, _ = api_avatar_url(email, int(24))
repo_info = {
"type": "mine",
@ -125,6 +126,7 @@ class ReposView(APIView):
"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 @@ class ReposView(APIView):
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 @@ class ReposView(APIView):
"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 @@ class ReposView(APIView):
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 @@ class ReposView(APIView):
"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 @@ class RepoView(APIView):
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 @@ class RepoView(APIView):
"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,

View File

@ -3390,6 +3390,9 @@ class FileDetailView(APIView):
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)