mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-14 06:11:16 +00:00
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;
|
||||
};
|
||||
|
||||
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 });
|
||||
};
|
||||
|
@@ -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;
|
Reference in New Issue
Block a user