1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-17 07:41:26 +00:00

Viewer add (#3084)

This commit is contained in:
cainiao222
2019-03-11 21:08:25 +08:00
committed by Daniel Pan
parent e31a67dea6
commit 599ea3bf5d
4 changed files with 512 additions and 14 deletions

View File

@@ -531,9 +531,9 @@
"integrity": "sha512-8KtjFl4D0vJBTl1H64ciXHz5oyUtqnnJI65wAa1IBKwA+xmF/++DWeV1i+O9/DK135ZVhrERfgW2EGvu50ZMNQ==" "integrity": "sha512-8KtjFl4D0vJBTl1H64ciXHz5oyUtqnnJI65wAa1IBKwA+xmF/++DWeV1i+O9/DK135ZVhrERfgW2EGvu50ZMNQ=="
}, },
"@seafile/seafile-editor": { "@seafile/seafile-editor": {
"version": "0.1.84", "version": "0.1.85",
"resolved": "https://registry.npmjs.org/@seafile/seafile-editor/-/seafile-editor-0.1.84.tgz", "resolved": "https://registry.npmjs.org/@seafile/seafile-editor/-/seafile-editor-0.1.85.tgz",
"integrity": "sha512-S/Ybh3NxPRQEogpnCEvA+/Ld9g+M9dTo1evt88zVIo0K27PqL9EEoTYFD5nsuth9aQaE3Bp8nfsaz/wUjg0JAQ==", "integrity": "sha512-NcksE2epv51X0+u6CpMSKxaKx4HVZz3hgsJOVz4onfPRDnuGmxVItalvDmCVu3T0UWfJd3Sq6Ub1oOIXCvg+lQ==",
"requires": { "requires": {
"@seafile/slate-react": "0.1.4", "@seafile/slate-react": "0.1.4",
"autoprefixer": "7.1.6", "autoprefixer": "7.1.6",

View File

@@ -7,7 +7,7 @@
"@babel/runtime": "^7.3.1", "@babel/runtime": "^7.3.1",
"@reach/router": "^1.2.0", "@reach/router": "^1.2.0",
"@seafile/resumablejs": "^1.1.8", "@seafile/resumablejs": "^1.1.8",
"@seafile/seafile-editor": "^0.1.84", "@seafile/seafile-editor": "^0.1.85",
"MD5": "^1.3.0", "MD5": "^1.3.0",
"antd": "^3.13.6", "antd": "^3.13.6",
"autoprefixer": "7.1.6", "autoprefixer": "7.1.6",

View File

@@ -0,0 +1,139 @@
import React from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants';
import { IconButton, ButtonGroup, CollabUsersButton } from '@seafile/seafile-editor/dist/components/topbarcomponent/editorToolBar';
import FileInfo from '@seafile/seafile-editor/dist/components/topbarcomponent/file-info';
const propTypes = {
hasDraft: PropTypes.bool.isRequired,
isDraft: PropTypes.bool.isRequired,
showFileHistory: PropTypes.bool.isRequired,
editorUtilities: PropTypes.object.isRequired,
collabUsers: PropTypes.array.isRequired,
fileInfo: PropTypes.object.isRequired,
fileTagList: PropTypes.array.isRequired,
relatedFiles: PropTypes.array.isRequired,
commentsNumber: PropTypes.number.isRequired,
toggleCommentList: PropTypes.func.isRequired,
toggleShareLinkDialog: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
toggleHistory: PropTypes.func.isRequired,
toggleNewDraft: PropTypes.func.isRequired,
toggleStar: PropTypes.func.isRequired,
backToParentDirectory: PropTypes.func.isRequired,
openDialogs: PropTypes.func.isRequired,
};
class MarkdownViewerToolbar extends React.Component {
constructor(props) {
super(props);
}
renderFirstToolbar() {
return (
<div className="sf-md-viewer-topbar-first d-flex justify-content-between">
<FileInfo toggleStar={this.props.toggleStar} editorUtilities={this.props.editorUtilities}
fileInfo={this.props.fileInfo}/>
{(this.props.hasDraft && !this.props.isDraft) &&
<div className='seafile-btn-view-review'>
<div className='tag tag-green'>{gettext('This file is in draft stage.')}
<a className="ml-2" onMouseDown={this.props.editorUtilities.goDraftPage}>{gettext('View draft')}</a></div>
</div>
}
<div className="topbar-btn-container">
{ (!this.props.hasDraft && !this.props.isDraft) &&
<button onMouseDown={this.props.toggleNewDraft} className="btn btn-success btn-new-draft">
{gettext('New draft')}</button>
}
{this.props.collabUsers.length > 0 && <CollabUsersButton className={'collab-users-dropdown'}
users={this.props.collabUsers} id={'usersButton'} />}
<ButtonGroup>
<IconButton id={'shareBtn'} text={gettext('Share')} icon={'fa fa-share-alt'}
onMouseDown={this.props.toggleShareLinkDialog}/>
{
this.props.commentsNumber > 0 ?
<button className="btn btn-icon btn-secondary btn-active" id="commentsNumber"
type="button" data-active="false" onMouseDown={this.props.toggleCommentList}>
<i className="fa fa-comments"></i>{' '}<span>{this.props.commentsNumber}</span>
</button>
:
<IconButton id={'commentsNumber'} text={gettext('Comments')} icon={'fa fa-comments'}
onMouseDown={this.props.toggleCommentList}/>
}
<IconButton text={gettext('Back to parent directory')} id={'parentDirectory'}
icon={'fa fa-folder-open'} onMouseDown={this.props.backToParentDirectory}/>
{
(!this.props.hasDraft && this.props.fileInfo.permission === 'rw')? <IconButton text={gettext('Edit')}
id={'editButton'} icon={'fa fa-edit'} onMouseDown={this.props.onEdit}/>: null
}
{
(this.props.showFileHistory) && (!this.props.isShowHistory && <IconButton id={'historyButton'}
text={gettext('File history')} onMouseDown={this.props.toggleHistory} icon={'fa fa-history'}/>)
}
</ButtonGroup>
</div>
</div>
);
}
renderSecondToolbar() {
const { relatedFiles, fileTagList } = this.props;
const openDialogs = this.props.openDialogs;
let relatedFileString = '';
if (relatedFiles) {
const length = relatedFiles.length;
if (length === 1) relatedFileString = 'Related file';
else if (length > 1) relatedFileString = 'Related files';
}
return(
<div className="sf-md-viewer-topbar-second d-flex justify-content-center">
{(fileTagList) && (fileTagList.length > 0 ?
<ul className="sf-files-tags">
{
fileTagList.map((item, index=0) => {
return (
<li key={index} className='sf-files-tag'>
<span className="file-tag-icon" style={{backgroundColor: item.tag_color}}></span>
<span className="file-tag-name" title={item.tag_name}>{item.tag_name}</span>
</li>
);
})
}
<li className='sf-files-tag'><span className="file-tag-name"
onClick={openDialogs.bind(this, 'tags')}>{gettext('Edit')}</span></li>
</ul>
:
<span className="no-file-tag edit-related-file"
onClick={openDialogs.bind(this, 'tags')}>{gettext('No tags')}</span>
)}
{relatedFiles &&
<div className="sf-related-files-bar">
{relatedFiles.length === 0 ?
<span className="edit-related-file no-related-file"
onClick={openDialogs.bind(this, 'related_files')}>{gettext('No related files')}</span>:
<React.Fragment>
<a href="#sf-releted-files">{relatedFiles.length}{' '}{gettext(relatedFileString)}</a>
<span className="edit-related-file" onClick={openDialogs.bind(this, 'related_files')}>
{gettext('Edit')}</span>
</React.Fragment>
}
</div>
}
</div>
);
}
render() {
return (
<div className="sf-md-viewer-topbar">
{this.renderFirstToolbar()}
{this.renderSecondToolbar()}
</div>
);
}
}
MarkdownViewerToolbar.propTypes = propTypes;
export default MarkdownViewerToolbar;

View File

@@ -8,7 +8,14 @@ import EditFileTagDialog from './components/dialog/edit-filetag-dialog';
import ListRelatedFileDialog from './components/dialog/list-related-file-dialog'; import ListRelatedFileDialog from './components/dialog/list-related-file-dialog';
import AddRelatedFileDialog from './components/dialog/add-related-file-dialog'; import AddRelatedFileDialog from './components/dialog/add-related-file-dialog';
import ShareDialog from './components/dialog/share-dialog'; import ShareDialog from './components/dialog/share-dialog';
import MarkdownViewerSlate from '@seafile/seafile-editor/dist/viewer/markdown-viewer-slate';
import io from "socket.io-client";
import toaster from "./components/toast";
import { serialize } from "@seafile/seafile-editor/dist/utils/slate2markdown";
import LocalDraftDialog from "@seafile/seafile-editor/dist/components/local-draft-dialog";
import MarkdownViewerToolbar from './components/toolbar/markdown-viewer-toolbar';
const CryptoJS = require('crypto-js');
const { repoID, repoName, filePath, fileName, mode, draftID, draftFilePath, draftOriginFilePath, isDraft, hasDraft, shareLinkExpireDaysMin, shareLinkExpireDaysMax } = window.app.pageOptions; const { repoID, repoName, filePath, fileName, mode, draftID, draftFilePath, draftOriginFilePath, isDraft, hasDraft, shareLinkExpireDaysMin, shareLinkExpireDaysMax } = window.app.pageOptions;
const { siteRoot, serviceUrl, seafileCollabServer } = window.app.config; const { siteRoot, serviceUrl, seafileCollabServer } = window.app.config;
const userInfo = window.app.userInfo; const userInfo = window.app.userInfo;
@@ -256,6 +263,11 @@ const editorUtilities = new EditorUtilities();
class MarkdownEditor extends React.Component { class MarkdownEditor extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.timer = null;
this.localDraft = '';
this.autoSave = false;
this.draftRichValue = '';
this.draftPlainValue = '';
this.state = { this.state = {
markdownContent: '', markdownContent: '',
loading: true, loading: true,
@@ -271,15 +283,98 @@ class MarkdownEditor extends React.Component {
lastModifier: '', lastModifier: '',
id: '', id: '',
}, },
editorMode: 'viewer',
collabServer: seafileCollabServer ? seafileCollabServer : null, collabServer: seafileCollabServer ? seafileCollabServer : null,
relatedFiles: [], relatedFiles: [],
fileTagList: [], fileTagList: [],
localDraftDialog: false,
showRelatedFileDialog: false, showRelatedFileDialog: false,
showEditFileTagDialog: false, showEditFileTagDialog: false,
showAddRelatedFileDialog: false, showAddRelatedFileDialog: false,
showMarkdownEditorDialog: false, showMarkdownEditorDialog: false,
showShareLinkDialog: false, showShareLinkDialog: false,
showDraftSaved: false,
collabUsers: userInfo ?
[{user: userInfo, is_editing: false}] : [],
isShowHistory: false,
isShowComments: false,
commentsNumber: null,
}; };
if (this.state.collabServer) {
const socket = io(this.state.collabServer);
this.socket = socket;
socket.on('presence', (data) => this.receivePresenceData(data));
socket.on('repo_update', (data) => this.receiveUpdateData(data));
socket.on('connect', () => {
this.socket_id = socket.id;
});
}
}
emitSwitchEditor = (is_editing=false) => {
if (userInfo && this.state.collabServer) {
const { repoID, path } = this.state.fileInfo;
this.socket.emit('presence', {
request: 'editing',
doc_id: CryptoJS.MD5(repoID+path).toString(),
user: userInfo,
is_editing,
});
}
}
receiveUpdateData (data) {
let currentTime = new Date();
if ((parseFloat(currentTime - this.lastModifyTime)/1000) <= 5) {
return;
}
editorUtilities.fileMetaData().then((res) => {
if (res.data.id !== this.state.fileInfo.id) {
toaster.notify(
<span>
{this.props.t('this_file_has_been_updated')}
<a href='' >{' '}{this.props.t('refresh')}</a>
</span>,
{id: 'repo_updated', duration: 3600});
}
});
}
receivePresenceData(data) {
switch(data.response) {
case 'user_join':
toaster.notify(`user ${data.user.name} joined`, {
duration: 3
});
return;
case 'user_left':
toaster.notify(`user ${data.user.name} left`, {
duration: 3
});
return;
case 'update_users':
for (var prop in data.users) {
if (data.users.hasOwnProperty(prop)) {
if (prop === this.socket_id) {
data.users[prop]['myself'] = true;
break;
}
}
}
this.setState({collabUsers: Object.values(data.users)});
return;
case 'user_editing':
toaster.danger(`user ${data.user.name} is editing this file!`, {
duration: 3
});
return;
default:
console.log('unknown response type: ' + data.response);
return;
}
} }
toggleCancel = () => { toggleCancel = () => {
@@ -292,6 +387,65 @@ class MarkdownEditor extends React.Component {
}); });
} }
setEditorMode = (type) => {
this.setState({
editorMode: type
})
}
setDraftValue = (type, value) => {
if (type === 'rich') {
this.draftRichValue = value
} else {
this.draftPlainValue = value;
}
}
setContent = (str) => {
this.setState({
markdownContent: str
});
}
checkDraft = () => {
let draftKey = editorUtilities.getDraftKey();
let draft = localStorage.getItem(draftKey);
let that = this;
if (draft) {
that.setState({
localDraftDialog: true,
});
that.localDraft = draft;
localStorage.removeItem(draftKey);
}
}
useDraft = () => {
this.setState({
localDraftDialog: false,
loading: false,
markdownContent: this.localDraft,
editorMode: 'rich',
});
this.emitSwitchEditor(true);
}
deleteDraft = () => {
if (this.state.localDraftDialog) {
this.setState({
localDraftDialog: false,
loading: false,
});
} else {
let draftKey = editorUtilities.getDraftKey();
localStorage.removeItem(draftKey);
}
}
clearTimer = () => {
clearTimeout(this.timer);
this.timer = null;
}
closeAddRelatedFileDialog = () => { closeAddRelatedFileDialog = () => {
this.setState({ this.setState({
showAddRelatedFileDialog: false, showAddRelatedFileDialog: false,
@@ -340,6 +494,18 @@ class MarkdownEditor extends React.Component {
} }
} }
componentWillUnmount() {
this.socket.emit('repo_update', {
request: 'unwatch_update',
repo_id: this.props.editorUtilities.repoID,
user: {
name: this.props.editorUtilities.name,
username: this.props.editorUtilities.username,
contact_email: this.props.editorUtilities.contact_email,
},
});
}
componentDidMount() { componentDidMount() {
seafileAPI.getFileInfo(repoID, filePath).then((res) => { seafileAPI.getFileInfo(repoID, filePath).then((res) => {
@@ -361,15 +527,46 @@ class MarkdownEditor extends React.Component {
seafileAPI.getFileDownloadLink(repoID, filePath).then((res) => { seafileAPI.getFileDownloadLink(repoID, filePath).then((res) => {
const downLoadUrl = res.data; const downLoadUrl = res.data;
seafileAPI.getFileContent(downLoadUrl).then((res) => { seafileAPI.getFileContent(downLoadUrl).then((res) => {
const contentLength = res.data.length;
let isBlankFile = (contentLength === 0 || contentLength === 1);
let hasPermission = (this.state.fileInfo.permission === 'rw');
let isEditMode = this.state.mode;
this.setState({ this.setState({
markdownContent: res.data, markdownContent: res.data,
loading: false loading: false,
// Goto rich edit page
// First, the user has the relevant permissions, otherwise he can only enter the viewer interface or cannot access
// case1: If file is draft file
// case2: If mode == 'edit' and the file has no draft
// case3: The length of markDownContent is 1 when clear all content in editor and the file has no draft
editorMode: (hasPermission && (isDraft || (isEditMode && !hasDraft) || (isBlankFile && !hasDraft))) ? 'rich' : 'viewer',
}); });
}); });
}); });
}); });
if (userInfo && this.socket) {
const { repoID, path } = this.state.fileInfo;
this.socket.emit('presence', {
request: 'join_room',
doc_id: CryptoJS.MD5(repoID+path).toString(),
user: userInfo
});
this.socket.emit('repo_update', {
request: 'watch_update',
repo_id: editorUtilities.repoID,
user: {
name: editorUtilities.name,
username: editorUtilities.username,
contact_email: editorUtilities.contact_email,
},
});
}
this.checkDraft();
this.listRelatedFiles(); this.listRelatedFiles();
this.listFileTags(); this.listFileTags();
this.getCommentsNumber();
} }
listRelatedFiles = () => { listRelatedFiles = () => {
@@ -400,7 +597,99 @@ class MarkdownEditor extends React.Component {
this.listFileTags(); this.listFileTags();
} }
setFileInfoMtime = (fileInfo) => {
this.setState({
fileInfo: Object.assign({}, this.state.fileInfo, { mtime: fileInfo.mtime, id: fileInfo.id, lastModifier: fileInfo.last_modifier_name })
});
};
toggleStar = () => {
let starrd = this.state.fileInfo.starred;
if (starrd) {
editorUtilities.unStarItem().then((response) => {
this.setState({
fileInfo: Object.assign({}, this.state.fileInfo, {starred: !starrd})
});
});
} else if (!starrd) {
editorUtilities.starItem().then((response) => {
this.setState({
fileInfo: Object.assign({}, this.state.fileInfo, {starred: !starrd})
});
});
}
}
autoSaveDraft = () => {
let that = this;
if (that.timer) {
return;
} else {
that.timer = setTimeout(() => {
let str = '';
if (this.state.editorMode == 'rich') {
let value = this.draftRichValue;
str = serialize(value.toJSON());
}
else if (this.state.editorMode == 'plain') {
str = this.draftPlainValue;
}
let draftKey = editorUtilities.getDraftKey();
localStorage.setItem(draftKey, str);
that.setState({
showDraftSaved: true
});
setTimeout(() => {
that.setState({
showDraftSaved: false
});
}, 3000);
that.timer = null;
}, 60000);
}
}
backToParentDirectory = () => {
window.location.href = editorUtilities.getParentDectionaryUrl();
}
onEdit = (event) => {
event.preventDefault();
this.setEditorMode('rich');
}
toggleShareLinkDialog = () => {
this.openDialogs('share_link');
}
toggleHistory = () => {
this.setState({ isShowHistory: !this.state.isShowHistory });
}
toggleCommentList = () => {
if (this.state.isShowHistory) {
this.setState({ isShowHistory: false, isShowComments: true });
}
else {
this.setState({ isShowComments: !this.state.isShowComments });
}
}
getCommentsNumber = () => {
editorUtilities.getCommentsNumber().then((res) => {
let commentsNumber = res.data[Object.getOwnPropertyNames(res.data)[0]];
this.setState({
commentsNumber: commentsNumber
});
});
}
onCommentAdded = () => {
this.getCommentsNumber();
}
render() { render() {
let component;
if (this.state.loading) { if (this.state.loading) {
return ( return (
<div className="empty-loading-page"> <div className="empty-loading-page">
@@ -408,26 +697,96 @@ class MarkdownEditor extends React.Component {
</div> </div>
); );
} else if (this.state.mode === 'editor') { } else if (this.state.mode === 'editor') {
return ( if (this.state.editorMode === 'viewer') {
<React.Fragment> component = (
<SeafileEditor <div className="seafile-md-viewer d-flex flex-column">
<MarkdownViewerToolbar
hasDraft={hasDraft}
isDraft={isDraft}
editorUtilities={editorUtilities}
collabUsers={this.state.collabUsers}
fileInfo={this.state.fileInfo}
toggleStar={this.toggleStar}
backToParentDirectory={this.backToParentDirectory}
openDialogs={this.openDialogs}
fileTagList={this.state.fileTagList}
relatedFiles={this.state.relatedFiles}
commentsNumber={this.state.commentsNumber}
toggleCommentList={this.toggleCommentList}
toggleShareLinkDialog={this.toggleShareLinkDialog}
onEdit={this.onEdit}
showFileHistory={true}
toggleHistory={this.toggleHistory}
toggleNewDraft={editorUtilities.createDraftFile}
/>
<MarkdownViewerSlate
fileInfo={this.state.fileInfo}
markdownContent={this.state.markdownContent}
editorUtilities={editorUtilities}
collabUsers={this.state.collabUsers}
showFileHistory={true}
setFileInfoMtime={this.setFileInfoMtime}
toggleStar={this.toggleStar}
setEditorMode={this.setEditorMode}
draftID={draftID}
isDraft={isDraft}
emitSwitchEditor={this.emitSwitchEditor}
hasDraft={hasDraft}
shareLinkExpireDaysMin={shareLinkExpireDaysMin}
shareLinkExpireDaysMax={shareLinkExpireDaysMax}
relatedFiles={this.state.relatedFiles}
siteRoot={siteRoot}
openDialogs={this.openDialogs}
fileTagList={this.state.fileTagList}
showDraftSaved={this.state.showDraftSaved}
isShowHistory={this.state.isShowHistory}
isShowComments={this.state.isShowComments}
onCommentAdded={this.onCommentAdded}
commentsNumber={this.state.commentsNumber}
getCommentsNumber={this.getCommentsNumber}
toggleHistory={this.toggleHistory}
toggleCommentList={this.toggleCommentList}
/>
</div>
)
} else {
component = <SeafileEditor
fileInfo={this.state.fileInfo} fileInfo={this.state.fileInfo}
markdownContent={this.state.markdownContent} markdownContent={this.state.markdownContent}
editorUtilities={editorUtilities} editorUtilities={editorUtilities}
userInfo={this.state.collabServer ? userInfo : null} collabUsers={this.state.collabUsers}
collabServer={this.state.collabServer} setFileInfoMtime={this.setFileInfoMtime}
toggleStar={this.toggleStar}
showFileHistory={true} showFileHistory={true}
mode={mode} setEditorMode={this.setEditorMode}
setContent={this.setContent}
draftID={draftID} draftID={draftID}
isDraft={isDraft} isDraft={isDraft}
mode={this.state.mode}
emitSwitchEditor={this.emitSwitchEditor}
hasDraft={hasDraft} hasDraft={hasDraft}
shareLinkExpireDaysMin={shareLinkExpireDaysMin} editorMode={this.state.editorMode}
shareLinkExpireDaysMax={shareLinkExpireDaysMax}
relatedFiles={this.state.relatedFiles} relatedFiles={this.state.relatedFiles}
siteRoot={siteRoot} siteRoot={siteRoot}
autoSaveDraft={this.autoSaveDraft}
setDraftValue={this.setDraftValue}
clearTimer={this.clearTimer}
openDialogs={this.openDialogs} openDialogs={this.openDialogs}
fileTagList={this.state.fileTagList} fileTagList={this.state.fileTagList}
deleteDraft={this.deleteDraft}
showDraftSaved={this.state.showDraftSaved}
/> />
}
return (
<React.Fragment>
{this.state.localDraftDialog?
<LocalDraftDialog
localDraftDialog={this.state.localDraftDialog}
deleteDraft={this.deleteDraft}
useDraft={this.useDraft}/>:
null}
{component}
{this.state.showMarkdownEditorDialog && ( {this.state.showMarkdownEditorDialog && (
<React.Fragment> <React.Fragment>
{this.state.showRelatedFileDialog && {this.state.showRelatedFileDialog &&
@@ -485,4 +844,4 @@ class MarkdownEditor extends React.Component {
} }
} }
export default MarkdownEditor; export default MarkdownEditor;