mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-01 23:38:37 +00:00
parent
5c2f05ee8d
commit
6ed1fe58bc
51
frontend/src/components/dirent-detail/detail-item/index.css
Normal file
51
frontend/src/components/dirent-detail/detail-item/index.css
Normal 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;
|
||||||
|
}
|
36
frontend/src/components/dirent-detail/detail-item/index.js
Normal file
36
frontend/src/components/dirent-detail/detail-item/index.js
Normal 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;
|
||||||
|
|
@ -44,6 +44,14 @@ class DetailListView extends React.Component {
|
|||||||
return position;
|
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 = () => {
|
onEditFileTagToggle = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isEditFileTagShow: !this.state.isEditFileTagShow
|
isEditFileTagShow: !this.state.isEditFileTagShow
|
||||||
@ -55,14 +63,6 @@ class DetailListView extends React.Component {
|
|||||||
this.props.onFileTagChanged(this.props.dirent, direntPath);
|
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 = () => {
|
toggleExtraMetadataPropertiesDialog = () => {
|
||||||
this.setState({ isShowMetadataExtraProperties: !this.state.isShowMetadataExtraProperties });
|
this.setState({ isShowMetadataExtraProperties: !this.state.isShowMetadataExtraProperties });
|
||||||
};
|
};
|
||||||
|
@ -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;
|
|
@ -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;
|
@ -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;
|
||||||
|
}
|
@ -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;
|
@ -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;
|
@ -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
|
||||||
|
}
|
121
frontend/src/components/dirent-detail/dirent-details/index.js
Normal file
121
frontend/src/components/dirent-detail/dirent-details/index.js
Normal 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;
|
@ -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);
|
||||||
|
};
|
45
frontend/src/components/dirent-detail/header/index.css
Normal file
45
frontend/src/components/dirent-detail/header/index.css
Normal 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;
|
||||||
|
}
|
28
frontend/src/components/dirent-detail/header/index.js
Normal file
28
frontend/src/components/dirent-detail/header/index.js
Normal 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;
|
33
frontend/src/components/dirent-detail/index.css
Normal file
33
frontend/src/components/dirent-detail/index.css
Normal 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;
|
||||||
|
}
|
9
frontend/src/components/dirent-detail/index.js
Normal file
9
frontend/src/components/dirent-detail/index.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import LibDetail from './lib-details';
|
||||||
|
import DirentDetail from './dirent-details';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
export {
|
||||||
|
LibDetail,
|
||||||
|
DirentDetail,
|
||||||
|
};
|
@ -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;
|
|
62
frontend/src/components/dirent-detail/lib-details/index.js
Normal file
62
frontend/src/components/dirent-detail/lib-details/index.js
Normal 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;
|
@ -2,45 +2,6 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.detail-body {
|
||||||
|
@ -183,7 +183,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
width: 300px;
|
width: 400px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
box-shadow: -1px 0 3px 0 #ccc;
|
box-shadow: -1px 0 3px 0 #ccc;
|
||||||
animation: move .5s ease-in-out 1;
|
animation: move .5s ease-in-out 1;
|
||||||
|
@ -33,7 +33,6 @@ class GroupContainerRight extends Component {
|
|||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
summaryConfigs={summaryConfigs}
|
summaryConfigs={summaryConfigs}
|
||||||
getTableContentRect={this.props.getTableContentRect}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -50,7 +49,6 @@ GroupContainerRight.propTypes = {
|
|||||||
height: PropTypes.number,
|
height: PropTypes.number,
|
||||||
groupOffsetLeft: PropTypes.number,
|
groupOffsetLeft: PropTypes.number,
|
||||||
lastFrozenColumnKey: PropTypes.string,
|
lastFrozenColumnKey: PropTypes.string,
|
||||||
getTableContentRect: PropTypes.func,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GroupContainerRight;
|
export default GroupContainerRight;
|
||||||
|
@ -10,9 +10,8 @@ class GroupHeaderCell extends React.PureComponent {
|
|||||||
fixedFrozenDOMs = (scrollLeft, scrollTop) => {
|
fixedFrozenDOMs = (scrollLeft, scrollTop) => {
|
||||||
if (this.headerCell) {
|
if (this.headerCell) {
|
||||||
const { firstColumnWidth, groupOffsetLeft } = this.props;
|
const { firstColumnWidth, groupOffsetLeft } = this.props;
|
||||||
const tableContentLeft = this.props.getTableContentRect();
|
|
||||||
this.headerCell.style.position = 'fixed';
|
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';
|
this.headerCell.style.marginTop = (-scrollTop) + 'px';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -64,7 +63,6 @@ GroupHeaderCell.propTypes = {
|
|||||||
groupOffsetLeft: PropTypes.number,
|
groupOffsetLeft: PropTypes.number,
|
||||||
summary: PropTypes.object,
|
summary: PropTypes.object,
|
||||||
summaryMethod: PropTypes.string,
|
summaryMethod: PropTypes.string,
|
||||||
getTableContentRect: PropTypes.func,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GroupHeaderCell;
|
export default GroupHeaderCell;
|
||||||
|
@ -57,7 +57,6 @@ class GroupHeaderRight extends Component {
|
|||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
summary={summary}
|
summary={summary}
|
||||||
summaryMethod={summaryMethod}
|
summaryMethod={summaryMethod}
|
||||||
getTableContentRect={this.props.getTableContentRect}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -79,7 +78,6 @@ GroupHeaderRight.propTypes = {
|
|||||||
lastFrozenColumnKey: PropTypes.string,
|
lastFrozenColumnKey: PropTypes.string,
|
||||||
columns: PropTypes.array,
|
columns: PropTypes.array,
|
||||||
summaryConfigs: PropTypes.object,
|
summaryConfigs: PropTypes.object,
|
||||||
getTableContentRect: PropTypes.func,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GroupHeaderRight;
|
export default GroupHeaderRight;
|
||||||
|
@ -133,7 +133,6 @@ class GroupContainer extends Component {
|
|||||||
lastFrozenColumnKey={lastFrozenColumnKey}
|
lastFrozenColumnKey={lastFrozenColumnKey}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
summaryConfigs={summaryConfigs}
|
summaryConfigs={summaryConfigs}
|
||||||
getTableContentRect={this.props.getTableContentRect}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -157,7 +156,6 @@ GroupContainer.propTypes = {
|
|||||||
scrollLeft: PropTypes.number,
|
scrollLeft: PropTypes.number,
|
||||||
maxLevel: PropTypes.number,
|
maxLevel: PropTypes.number,
|
||||||
summaryConfigs: PropTypes.object,
|
summaryConfigs: PropTypes.object,
|
||||||
getTableContentRect: PropTypes.func,
|
|
||||||
onExpandGroupToggle: PropTypes.func,
|
onExpandGroupToggle: PropTypes.func,
|
||||||
updateSummaryConfig: PropTypes.func,
|
updateSummaryConfig: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
@ -790,7 +790,6 @@ class GroupBody extends Component {
|
|||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
folding={folding}
|
folding={folding}
|
||||||
lastFrozenColumnKey={lastFrozenColumnKey}
|
lastFrozenColumnKey={lastFrozenColumnKey}
|
||||||
getTableContentRect={this.props.getTableContentRect}
|
|
||||||
onExpandGroupToggle={this.onExpandGroupToggle}
|
onExpandGroupToggle={this.onExpandGroupToggle}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -136,7 +136,7 @@ class RecordsFooter extends React.Component {
|
|||||||
const recordWidth = (isLoadingMore || hasMore ? SEQUENCE_COLUMN_WIDTH + columns[0].width : SEQUENCE_COLUMN_WIDTH) + groupOffsetLeft;
|
const recordWidth = (isLoadingMore || hasMore ? SEQUENCE_COLUMN_WIDTH + columns[0].width : SEQUENCE_COLUMN_WIDTH) + groupOffsetLeft;
|
||||||
|
|
||||||
return (
|
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 }}>
|
<div className="rows-record d-flex text-nowrap" style={{ width: recordWidth }}>
|
||||||
<span>{this.getRecord()}</span>
|
<span>{this.getRecord()}</span>
|
||||||
{!isLoadingMore && hasMore &&
|
{!isLoadingMore && hasMore &&
|
||||||
|
@ -65,8 +65,7 @@ const ViewToolBar = ({ metadataViewId }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='sf-metadata-tool'
|
className="sf-metadata-tool"
|
||||||
// style={{ zIndex: Z_INDEX.TABLE_HEADER, transform: 'translateZ(1000px)' }}
|
|
||||||
onClick={onHeaderClick}
|
onClick={onHeaderClick}
|
||||||
>
|
>
|
||||||
<div className="sf-metadata-tool-left-operations">
|
<div className="sf-metadata-tool-left-operations">
|
||||||
|
@ -10,6 +10,8 @@ class RepoInfo {
|
|||||||
this.owner_name = object.owner_name;
|
this.owner_name = object.owner_name;
|
||||||
this.owner_email = object.owner_email;
|
this.owner_email = object.owner_email;
|
||||||
this.owner_contact_email = object.owner_contact_email;
|
this.owner_contact_email = object.owner_contact_email;
|
||||||
|
this.owner_avatar = object.owner_avatar || '';
|
||||||
|
|
||||||
// is repo shared admin;
|
// is repo shared admin;
|
||||||
// is repo shared admin && is one of current ordinary group's admins;
|
// is repo shared admin && is one of current ordinary group's admins;
|
||||||
// is one of current group owned group's admins;
|
// is one of current group owned group's admins;
|
||||||
|
@ -4,17 +4,26 @@ class Repo {
|
|||||||
constructor(object) {
|
constructor(object) {
|
||||||
this.repo_id = object.repo_id;
|
this.repo_id = object.repo_id;
|
||||||
this.repo_name = object.repo_name;
|
this.repo_name = object.repo_name;
|
||||||
|
this.repo_type = object.repo_type;
|
||||||
this.permission = object.permission;
|
this.permission = object.permission;
|
||||||
this.size_original = object.size;
|
this.size_original = object.size;
|
||||||
this.size = Utils.bytesToSize(object.size);
|
this.size = Utils.bytesToSize(object.size);
|
||||||
|
|
||||||
|
// owner info
|
||||||
this.owner_name = object.owner_name;
|
this.owner_name = object.owner_name;
|
||||||
this.owner_email = object.owner_email;
|
this.owner_email = object.owner_email;
|
||||||
this.owner_contact_email = object.owner_contact_email;
|
this.owner_contact_email = object.owner_contact_email;
|
||||||
|
this.owner_avatar = object.owner_avatar || '';
|
||||||
|
|
||||||
this.encrypted = object.encrypted;
|
this.encrypted = object.encrypted;
|
||||||
|
|
||||||
|
// last_modified: last modified time
|
||||||
this.last_modified = object.last_modified;
|
this.last_modified = object.last_modified;
|
||||||
this.modifier_contact_email = object.modifier_contact_email;
|
this.modifier_contact_email = object.modifier_contact_email;
|
||||||
this.modifier_email = object.modifier_email;
|
this.modifier_email = object.modifier_email;
|
||||||
this.modifier_name = object.modifier_name;
|
this.modifier_name = object.modifier_name;
|
||||||
|
this.modifier_avatar = object.modifier_avatar;
|
||||||
|
|
||||||
this.type = object.type;
|
this.type = object.type;
|
||||||
this.starred = object.starred;
|
this.starred = object.starred;
|
||||||
this.monitored = object.monitored;
|
this.monitored = object.monitored;
|
||||||
@ -23,6 +32,11 @@ class Repo {
|
|||||||
if (object.is_admin != undefined) {
|
if (object.is_admin != undefined) {
|
||||||
this.is_admin = object.is_admin;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,8 +2,7 @@ import React, { Fragment } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { gettext } from '../../utils/constants';
|
import { gettext } from '../../utils/constants';
|
||||||
import CurDirPath from '../../components/cur-dir-path';
|
import CurDirPath from '../../components/cur-dir-path';
|
||||||
import DirentDetail from '../../components/dirent-detail/dirent-details';
|
import { LibDetail, DirentDetail } from '../../components/dirent-detail';
|
||||||
import LibDetail from '../../components/dirent-detail/lib-details';
|
|
||||||
import DirColumnView from '../../components/dir-view-mode/dir-column-view';
|
import DirColumnView from '../../components/dir-view-mode/dir-column-view';
|
||||||
import ToolbarForSelectedDirents from '../../components/toolbar/selected-dirents-toolbar';
|
import ToolbarForSelectedDirents from '../../components/toolbar/selected-dirents-toolbar';
|
||||||
|
|
||||||
@ -318,31 +317,29 @@ class LibContentContainer extends React.Component {
|
|||||||
isDirentDetailShow={this.props.isDirentDetailShow}
|
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>
|
||||||
</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>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -126,7 +126,7 @@ class OrgInfo extends Component {
|
|||||||
<span>{Utils.bytesToSize(user_default_quota)}</span>
|
<span>{Utils.bytesToSize(user_default_quota)}</span>
|
||||||
<span
|
<span
|
||||||
title={gettext('Edit')}
|
title={gettext('Edit')}
|
||||||
className={`sf3-font sf3-font-rename attr-action-icon`}
|
className="sf3-font sf3-font-rename attr-action-icon"
|
||||||
onClick={this.toggleSetUserDefaultQuotaDialog}>
|
onClick={this.toggleSetUserDefaultQuotaDialog}>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
@ -612,8 +612,8 @@ a, a:hover { color: #ec8000; }
|
|||||||
.side-nav {
|
.side-nav {
|
||||||
flex:auto;
|
flex:auto;
|
||||||
display:flex;
|
display:flex;
|
||||||
flex-direction:column;
|
flex-direction: column;
|
||||||
justify-content:space-between; /* make .side-nav-footer on the bottom */
|
justify-content: space-between; /* make .side-nav-footer on the bottom */
|
||||||
overflow:hidden; /* for ff */
|
overflow:hidden; /* for ff */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.timeutils import timestamp_to_isoformat_timestr
|
||||||
from seahub.utils.repo import get_repo_owner, is_repo_admin, \
|
from seahub.utils.repo import get_repo_owner, is_repo_admin, \
|
||||||
repo_has_been_shared_out, normalize_repo_status_code
|
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
|
from seahub.settings import ENABLE_STORAGE_CLASSES
|
||||||
|
|
||||||
@ -116,7 +117,7 @@ class ReposView(APIView):
|
|||||||
|
|
||||||
if is_wiki_repo(r):
|
if is_wiki_repo(r):
|
||||||
continue
|
continue
|
||||||
|
url, _, _ = api_avatar_url(email, int(24))
|
||||||
|
|
||||||
repo_info = {
|
repo_info = {
|
||||||
"type": "mine",
|
"type": "mine",
|
||||||
@ -125,6 +126,7 @@ class ReposView(APIView):
|
|||||||
"owner_email": email,
|
"owner_email": email,
|
||||||
"owner_name": email2nickname(email),
|
"owner_name": email2nickname(email),
|
||||||
"owner_contact_email": email2contact_email(email),
|
"owner_contact_email": email2contact_email(email),
|
||||||
|
"owner_avatar": url,
|
||||||
"last_modified": timestamp_to_isoformat_timestr(r.last_modify),
|
"last_modified": timestamp_to_isoformat_timestr(r.last_modify),
|
||||||
"modifier_email": r.last_modifier,
|
"modifier_email": r.last_modifier,
|
||||||
"modifier_name": nickname_dict.get(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_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, '')
|
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 = {
|
repo_info = {
|
||||||
"type": "shared",
|
"type": "shared",
|
||||||
@ -201,6 +204,7 @@ class ReposView(APIView):
|
|||||||
"owner_email": owner_email,
|
"owner_email": owner_email,
|
||||||
"owner_name": owner_name,
|
"owner_name": owner_name,
|
||||||
"owner_contact_email": owner_contact_email,
|
"owner_contact_email": owner_contact_email,
|
||||||
|
"owner_avatar": url,
|
||||||
"size": r.size,
|
"size": r.size,
|
||||||
"encrypted": r.encrypted,
|
"encrypted": r.encrypted,
|
||||||
"permission": r.permission,
|
"permission": r.permission,
|
||||||
@ -299,6 +303,7 @@ class ReposView(APIView):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
repo_owner = repo_id_owner_dict[r.repo_id]
|
repo_owner = repo_id_owner_dict[r.repo_id]
|
||||||
|
url, _, _ = api_avatar_url(repo_owner, int(24))
|
||||||
repo_info = {
|
repo_info = {
|
||||||
"type": "public",
|
"type": "public",
|
||||||
"repo_id": r.repo_id,
|
"repo_id": r.repo_id,
|
||||||
@ -310,6 +315,7 @@ class ReposView(APIView):
|
|||||||
"owner_email": repo_owner,
|
"owner_email": repo_owner,
|
||||||
"owner_name": nickname_dict.get(repo_owner, ''),
|
"owner_name": nickname_dict.get(repo_owner, ''),
|
||||||
"owner_contact_email": contact_email_dict.get(repo_owner, ''),
|
"owner_contact_email": contact_email_dict.get(repo_owner, ''),
|
||||||
|
"owner_avatar": url,
|
||||||
"size": r.size,
|
"size": r.size,
|
||||||
"encrypted": r.encrypted,
|
"encrypted": r.encrypted,
|
||||||
"permission": r.permission,
|
"permission": r.permission,
|
||||||
@ -363,13 +369,14 @@ class RepoView(APIView):
|
|||||||
lib_need_decrypt = True
|
lib_need_decrypt = True
|
||||||
|
|
||||||
repo_owner = get_repo_owner(request, repo_id)
|
repo_owner = get_repo_owner(request, repo_id)
|
||||||
|
url, _, _ = api_avatar_url(repo_owner, int(24))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
has_been_shared_out = repo_has_been_shared_out(request, repo_id)
|
has_been_shared_out = repo_has_been_shared_out(request, repo_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
has_been_shared_out = False
|
has_been_shared_out = False
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"repo_id": repo.id,
|
"repo_id": repo.id,
|
||||||
"repo_name": repo.name,
|
"repo_name": repo.name,
|
||||||
@ -377,6 +384,7 @@ class RepoView(APIView):
|
|||||||
"owner_email": repo_owner,
|
"owner_email": repo_owner,
|
||||||
"owner_name": email2nickname(repo_owner),
|
"owner_name": email2nickname(repo_owner),
|
||||||
"owner_contact_email": email2contact_email(repo_owner),
|
"owner_contact_email": email2contact_email(repo_owner),
|
||||||
|
"owner_avatar": url,
|
||||||
|
|
||||||
"size": repo.size,
|
"size": repo.size,
|
||||||
"encrypted": repo.encrypted,
|
"encrypted": repo.encrypted,
|
||||||
|
@ -3390,6 +3390,9 @@ class FileDetailView(APIView):
|
|||||||
entry["last_modifier_email"] = latest_contributor
|
entry["last_modifier_email"] = latest_contributor
|
||||||
entry["last_modifier_name"] = email2nickname(latest_contributor)
|
entry["last_modifier_name"] = email2nickname(latest_contributor)
|
||||||
entry["last_modifier_contact_email"] = email2contact_email(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:
|
try:
|
||||||
file_size = get_file_size(real_repo_id, repo.version, obj_id)
|
file_size = get_file_size(real_repo_id, repo.version, obj_id)
|
||||||
|
Loading…
Reference in New Issue
Block a user