1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-01 23:20:51 +00:00

Wiki mode improve (#3100)

* repair internal link bug

* add tags for path

* add share&editTag feature

* add related-file for column-mode

* repair edit tag bug

* update implement logic
This commit is contained in:
杨顺强
2019-03-15 10:10:24 +08:00
committed by Daniel Pan
parent 81c131ba07
commit 7502ce83df
7 changed files with 288 additions and 49 deletions

View File

@@ -1,7 +1,10 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Link } from '@reach/router'; import { Link } from '@reach/router';
import { UncontrolledTooltip } from 'reactstrap';
import { siteRoot, gettext } from '../../utils/constants'; import { siteRoot, gettext } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import FileTag from '../../models/file-tag';
import InternalLinkDialog from '../dialog/internal-link-dialog'; import InternalLinkDialog from '../dialog/internal-link-dialog';
const propTypes = { const propTypes = {
@@ -12,6 +15,7 @@ const propTypes = {
pathPrefix: PropTypes.array, pathPrefix: PropTypes.array,
repoID: PropTypes.string.isRequired, repoID: PropTypes.string.isRequired,
isViewFile: PropTypes.bool, isViewFile: PropTypes.bool,
fileTags: PropTypes.array.isRequired,
}; };
class DirPath extends React.Component { class DirPath extends React.Component {
@@ -51,8 +55,16 @@ class DirPath extends React.Component {
} }
render() { render() {
let { currentPath, repoName } = this.props; let { currentPath, repoName, fileTags } = this.props;
let pathElem = this.turnPathToLink(currentPath); let pathElem = this.turnPathToLink(currentPath);
let tagTitle = '';
if (fileTags.length > 0) {
fileTags.forEach(item => {
tagTitle += item.name + ' ';
});
}
return ( return (
<div className="path-container"> <div className="path-container">
{this.props.pathPrefix && this.props.pathPrefix.map((item, index) => { {this.props.pathPrefix && this.props.pathPrefix.map((item, index) => {
@@ -68,8 +80,7 @@ class DirPath extends React.Component {
<Link to={siteRoot + 'my-libs/'} className="normal" onClick={() => this.onTabNavClick.bind(this, 'my-libs')}>{gettext('Libraries')}</Link> <Link to={siteRoot + 'my-libs/'} className="normal" onClick={() => this.onTabNavClick.bind(this, 'my-libs')}>{gettext('Libraries')}</Link>
<span className="path-split">/</span> <span className="path-split">/</span>
</Fragment> </Fragment>
) )}
}
{!this.props.pathPrefix && ( {!this.props.pathPrefix && (
<Fragment> <Fragment>
<a href={siteRoot + 'my-libs/'} className="normal" onClick={() => this.onTabNavClick.bind(this, 'my-libs')}>{gettext('Libraries')}</a> <a href={siteRoot + 'my-libs/'} className="normal" onClick={() => this.onTabNavClick.bind(this, 'my-libs')}>{gettext('Libraries')}</a>
@@ -81,12 +92,22 @@ class DirPath extends React.Component {
<a className="path-link" data-path="/" onClick={this.onPathClick}>{repoName}</a> <a className="path-link" data-path="/" onClick={this.onPathClick}>{repoName}</a>
} }
{pathElem} {pathElem}
{ this.props.isViewFile && {this.props.isViewFile &&
<InternalLinkDialog <InternalLinkDialog
repoID={this.props.repoID} repoID={this.props.repoID}
path={this.props.currentPath} path={this.props.currentPath}
/> />
} }
{(this.props.isViewFile && fileTags.length !== 0) &&
<span id='column-mode-file-tags' className="tag-list tag-list-stacked align-middle ml-1">
{fileTags.map((fileTag, index) => {
return (<span className="file-tag" key={fileTag.id} style={{zIndex: index, backgroundColor: fileTag.color}}></span>)
})}
<UncontrolledTooltip target="column-mode-file-tags" placement="bottom">
{tagTitle}
</UncontrolledTooltip>
</span>
}
</div> </div>
); );
} }

View File

@@ -13,6 +13,7 @@ const propTypes = {
pathPrefix: PropTypes.array, pathPrefix: PropTypes.array,
isViewFile: PropTypes.bool, isViewFile: PropTypes.bool,
updateUsedRepoTags: PropTypes.func.isRequired, updateUsedRepoTags: PropTypes.func.isRequired,
fileTags: PropTypes.array.isRequired,
}; };
class CurDirPath extends React.Component { class CurDirPath extends React.Component {
@@ -28,6 +29,7 @@ class CurDirPath extends React.Component {
onTabNavClick={this.props.onTabNavClick} onTabNavClick={this.props.onTabNavClick}
repoID={this.props.repoID} repoID={this.props.repoID}
isViewFile={this.props.isViewFile} isViewFile={this.props.isViewFile}
fileTags={this.props.fileTags}
/> />
<DirTool <DirTool
repoID={this.props.repoID} repoID={this.props.repoID}

View File

@@ -86,6 +86,11 @@ class DirentDetail extends React.Component {
} }
} }
onFileTagChanged = (dirent, direntPath) => {
this.updateDetailView(dirent, direntPath);
this.props.onFileTagChanged(dirent, direntPath);
}
onRelatedFileChange = () => { onRelatedFileChange = () => {
let { dirent, path } = this.props; let { dirent, path } = this.props;
let direntPath = Utils.joinPath(path, dirent.name); let direntPath = Utils.joinPath(path, dirent.name);
@@ -121,7 +126,7 @@ class DirentDetail extends React.Component {
direntDetail={this.state.direntDetail} direntDetail={this.state.direntDetail}
fileTagList={this.state.fileTagList} fileTagList={this.state.fileTagList}
relatedFiles={this.state.relatedFiles} relatedFiles={this.state.relatedFiles}
onFileTagChanged={this.props.onFileTagChanged} onFileTagChanged={this.onFileTagChanged}
onRelatedFileChange={this.onRelatedFileChange} onRelatedFileChange={this.onRelatedFileChange}
/> />
</div> </div>

View File

@@ -0,0 +1,174 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { DropdownToggle, Dropdown, DropdownMenu, DropdownItem, Tooltip } from 'reactstrap';
import { Utils } from '../../utils/utils';
import { gettext, siteRoot } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import ModalPotal from '../modal-portal';
import ShareDialog from '../dialog/share-dialog';
import EditFileTagDialog from '../dialog/edit-filetag-dialog';
import ListRelatedFileDialog from '../dialog/list-related-file-dialog';
import AddRelatedFileDialog from '../dialog/add-related-file-dialog';
const propTypes = {
path: PropTypes.string.isRequired,
repoID: PropTypes.string.isRequired,
userPerm: PropTypes.string.isRequired,
repoEncrypted: PropTypes.bool.isRequired,
enableDirPrivateShare: PropTypes.bool.isRequired,
isGroupOwnedRepo: PropTypes.bool.isRequired,
filePermission: PropTypes.bool.isRequired,
isDraft: PropTypes.bool.isRequired,
hasDraft: PropTypes.bool.isRequired,
fileTags: PropTypes.array.isRequired,
relatedFiles: PropTypes.array.isRequired,
onFileTagChanged: PropTypes.func.isRequired,
onRelatedFileChange: PropTypes.func.isRequired,
};
class ViewFileToolbar extends React.Component {
constructor(props) {
super(props);
this.state = {
isDraftMessageShow: false,
isMoreMenuShow: false,
isShareDialogShow: false,
isEditTagDialogShow: false,
isListRelatedFileDialogShow: false,
isAddRelatedFileDialogShow: false,
};
}
onEditClick = (e) => {
e.preventDefault();
let { path, repoID } = this.props;
let url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(path) + '?mode=edit';
window.open(url);
}
onNewDraft = (e) => {
e.preventDefault();
let { path, repoID } = this.props;
seafileAPI.createDraft(repoID, path).then(res => {
window.location.href = siteRoot + 'lib/' + res.data.origin_repo_id + '/file' + res.data.draft_file_path + '?mode=edit';
});
}
onDraftHover = () => {
this.setState({isDraftMessageShow: !this.state.isDraftMessageShow});
}
toggleMore = () => {
this.setState({isMoreMenuShow: !this.state.isMoreMenuShow});
}
onShareToggle = () => {
this.setState({isShareDialogShow: !this.state.isShareDialogShow});
}
onEditFileTagToggle = () => {
this.setState({isEditTagDialogShow: !this.state.isEditTagDialogShow});
}
onListRelatedFileToggle = () => {
this.setState({isListRelatedFileDialogShow: !this.state.isListRelatedFileDialogShow})
}
onAddRelatedFileToggle = () => {
this.setState({
isListRelatedFileDialogShow: !this.state.isListRelatedFileDialogShow,
isAddRelatedFileDialogShow: !this.state.isAddRelatedFileDialogShow,
});
}
render() {
let name = Utils.getFileName(this.props.path);
let dirent = { name: name };
return (
<Fragment>
<div className="dir-operation">
{(this.props.filePermission && !this.props.hasDraft) && (
<Fragment>
<button className="btn btn-secondary operation-item" title={gettext('Edit File')} onClick={this.onEditClick}>{gettext('Edit')}</button>
</Fragment>
)}
{/* default have read priv */}
{(!this.props.isDraft && !this.props.hasDraft) && (
<Fragment>
<button id="new-draft" className="btn btn-secondary operation-item" onClick={this.onNewDraft}>{gettext('New Draft')}</button>
<Tooltip target="new-draft" placement="bottom" isOpen={this.state.isDraftMessageShow} toggle={this.onDraftHover}>{gettext('Create a draft from this file, instead of editing it directly.')}</Tooltip>
</Fragment>
)}
{this.props.filePermission && (
<Dropdown isOpen={this.state.isMoreMenuShow} toggle={this.toggleMore}>
<DropdownToggle className='btn btn-secondary operation-item'>
{gettext('More')}
</DropdownToggle>
<DropdownMenu>
<DropdownItem onClick={this.onShareToggle}>{gettext('Share')}</DropdownItem>
<DropdownItem onClick={this.onEditFileTagToggle}>{gettext('Edit File Tag')}</DropdownItem>
<DropdownItem onClick={this.onListRelatedFileToggle}>{gettext('Add Relative File')}</DropdownItem>
</DropdownMenu>
</Dropdown>
)}
</div>
{this.state.isShareDialogShow && (
<ModalPotal>
<ShareDialog
itemType={'file'}
itemName={Utils.getFileName(this.props.path)}
itemPath={this.props.path}
repoID={this.props.repoID}
repoEncrypted={this.props.repoEncrypted}
enableDirPrivateShare={this.props.enableDirPrivateShare}
userPerm={this.props.userPerm}
isGroupOwnedRepo={this.props.isGroupOwnedRepo}
toggleDialog={this.onShareToggle}
/>
</ModalPotal>
)}
{this.state.isEditTagDialogShow && (
<ModalPotal>
<EditFileTagDialog
filePath={this.props.path}
repoID={this.props.repoID}
fileTagList={this.props.fileTags}
toggleCancel={this.onEditFileTagToggle}
onFileTagChanged={this.props.onFileTagChanged}
/>
</ModalPotal>
)}
{this.state.isListRelatedFileDialogShow &&
<ModalPotal>
<ListRelatedFileDialog
repoID={this.props.repoID}
filePath={this.props.path}
relatedFiles={this.props.relatedFiles}
toggleCancel={this.onListRelatedFileToggle}
addRelatedFileToggle={this.onAddRelatedFileToggle}
onRelatedFileChange={this.props.onRelatedFileChange}
/>
</ModalPotal>
}
{this.state.isAddRelatedFileDialogShow && (
<ModalPotal>
<AddRelatedFileDialog
dirent={dirent}
repoID={this.props.repoID}
filePath={this.props.path}
onRelatedFileChange={this.props.onRelatedFileChange}
toggleCancel={this.onAddRelatedFileToggle}
/>
</ModalPotal>
)}
</Fragment>
);
}
}
ViewFileToolbar.propTypes = propTypes;
export default ViewFileToolbar;

View File

@@ -28,6 +28,7 @@ const propTypes = {
hash: PropTypes.string, hash: PropTypes.string,
isDraft: PropTypes.bool.isRequired, isDraft: PropTypes.bool.isRequired,
hasDraft: PropTypes.bool.isRequired, hasDraft: PropTypes.bool.isRequired,
fileTags: PropTypes.array.isRequired,
goDraftPage: PropTypes.func.isRequired, goDraftPage: PropTypes.func.isRequired,
isFileLoading: PropTypes.bool.isRequired, isFileLoading: PropTypes.bool.isRequired,
filePermission: PropTypes.bool.isRequired, filePermission: PropTypes.bool.isRequired,
@@ -131,9 +132,11 @@ class LibContentContainer extends React.Component {
pathPrefix={this.props.pathPrefix} pathPrefix={this.props.pathPrefix}
currentPath={this.props.path} currentPath={this.props.path}
permission={this.props.repoPermission} permission={this.props.repoPermission}
isViewFile={this.props.isViewFile}
onTabNavClick={this.props.onTabNavClick} onTabNavClick={this.props.onTabNavClick}
onPathClick={this.onPathClick} onPathClick={this.onPathClick}
updateUsedRepoTags={this.props.updateUsedRepoTags} updateUsedRepoTags={this.props.updateUsedRepoTags}
fileTags={this.props.fileTags}
/> />
</div> </div>
<div className={`cur-view-content ${this.props.currentMode === 'column' ? 'view-mode-container' : ''}`}> <div className={`cur-view-content ${this.props.currentMode === 'column' ? 'view-mode-container' : ''}`}>

View File

@@ -1,19 +1,21 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Tooltip } from 'reactstrap'; import { gettext } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import { gettext, siteRoot } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import CommonToolbar from '../../components/toolbar/common-toolbar'; import CommonToolbar from '../../components/toolbar/common-toolbar';
import ViewModeToolbar from '../../components/toolbar/view-mode-toolbar'; import ViewModeToolbar from '../../components/toolbar/view-mode-toolbar';
import DirOperationToolBar from '../../components/toolbar/dir-operation-toolbar'; import DirOperationToolBar from '../../components/toolbar/dir-operation-toolbar';
import MutipleDirOperationToolbar from '../../components/toolbar/mutilple-dir-operation-toolbar'; import MutipleDirOperationToolbar from '../../components/toolbar/mutilple-dir-operation-toolbar';
import ViewFileToolbar from '../../components/toolbar/view-file-toolbar';
const propTypes = { const propTypes = {
isViewFile: PropTypes.bool.isRequired, isViewFile: PropTypes.bool.isRequired,
filePermission: PropTypes.bool.isRequired, // ture = 'rw' filePermission: PropTypes.bool.isRequired, // ture = 'rw'
isDraft: PropTypes.bool.isRequired, isDraft: PropTypes.bool.isRequired,
hasDraft: PropTypes.bool.isRequired, hasDraft: PropTypes.bool.isRequired,
fileTags: PropTypes.array.isRequired,
relatedFiles: PropTypes.array.isRequired,
onFileTagChanged: PropTypes.func.isRequired,
onRelatedFileChange: PropTypes.func.isRequired,
// side-panel // side-panel
onSideNavMenuClick: PropTypes.func.isRequired, onSideNavMenuClick: PropTypes.func.isRequired,
// mutiple-dir // mutiple-dir
@@ -45,52 +47,27 @@ const propTypes = {
class LibContentToolbar extends React.Component { class LibContentToolbar extends React.Component {
constructor(props) {
super(props);
this.state = {
isDraftMessageShow: false,
};
}
onEditClick = (e) => {
e.preventDefault();
let { path, repoID } = this.props;
let url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(path) + '?mode=edit';
window.open(url);
}
onNewDraft = (e) => {
e.preventDefault();
let { path, repoID } = this.props;
seafileAPI.createDraft(repoID, path).then(res => {
window.location.href = siteRoot + 'lib/' + res.data.origin_repo_id + '/file' + res.data.draft_file_path + '?mode=edit';
});
}
onDraftHover = () => {
this.setState({isDraftMessageShow: !this.state.isDraftMessageShow});
}
render() { render() {
if (this.props.isViewFile) { if (this.props.isViewFile) {
return ( return (
<Fragment> <Fragment>
<div className="cur-view-toolbar"> <div className="cur-view-toolbar">
<span className="sf2-icon-menu hidden-md-up d-md-none side-nav-toggle" title={gettext('Side Nav Menu')} onClick={this.props.onSideNavMenuClick}></span> <span className="sf2-icon-menu hidden-md-up d-md-none side-nav-toggle" title={gettext('Side Nav Menu')} onClick={this.props.onSideNavMenuClick}></span>
<div className="dir-operation"> <ViewFileToolbar
{(this.props.filePermission && !this.props.hasDraft) && ( path={this.props.path}
<Fragment> repoID={this.props.repoID}
<button className="btn btn-secondary operation-item" title={gettext('Edit File')} onClick={this.onEditClick}>{gettext('Edit')}</button> userPerm={this.props.userPerm}
</Fragment> repoEncrypted={this.props.repoEncrypted}
)} enableDirPrivateShare={this.props.enableDirPrivateShare}
{/* default have read priv */} isGroupOwnedRepo={this.props.isGroupOwnedRepo}
{(!this.props.isDraft && !this.props.hasDraft) && ( filePermission={this.props.filePermission}
<Fragment> isDraft={this.props.isDraft}
<button id="new-draft" className="btn btn-secondary operation-item" onClick={this.onNewDraft}>{gettext('New Draft')}</button> hasDraft={this.props.hasDraft}
<Tooltip target="new-draft" placement="bottom" isOpen={this.state.isDraftMessageShow} toggle={this.onDraftHover}>{gettext('Create a draft from this file, instead of editing it directly.')}</Tooltip> fileTags={this.props.fileTags}
</Fragment> relatedFiles={this.props.relatedFiles}
)} onFileTagChanged={this.props.onFileTagChanged}
</div> onRelatedFileChange={this.props.onRelatedFileChange}
/>
<ViewModeToolbar currentMode={this.props.currentMode} switchViewMode={this.props.switchViewMode}/> <ViewModeToolbar currentMode={this.props.currentMode} switchViewMode={this.props.switchViewMode}/>
</div> </div>
<CommonToolbar repoID={this.props.repoID} onSearchedClick={this.props.onSearchedClick} searchPlaceholder={gettext('Search files in this library')}/> <CommonToolbar repoID={this.props.repoID} onSearchedClick={this.props.onSearchedClick} searchPlaceholder={gettext('Search files in this library')}/>

View File

@@ -46,6 +46,8 @@ class LibContentView extends React.Component {
selectedDirentList: [], selectedDirentList: [],
isDraft: false, isDraft: false,
hasDraft: false, hasDraft: false,
fileTags: [],
relatedFiles: [],
draftID: '', draftID: '',
draftCounts: 0, draftCounts: 0,
usedRepoTags: [], usedRepoTags: [],
@@ -287,6 +289,27 @@ class LibContentView extends React.Component {
showFile = (filePath) => { showFile = (filePath) => {
let repoID = this.props.repoID; let repoID = this.props.repoID;
if (this.state.currentMode === 'column') {
seafileAPI.listFileTags(repoID, filePath).then(res => {
let fileTags = res.data.file_tags.map(item => {
return new FileTag(item);
});
this.setState({fileTags: fileTags});
});
seafileAPI.listRelatedFiles(repoID, filePath).then(res => {
let relatedFiles = res.data.related_files.map((relatedFile) => {
return relatedFile;
});
this.setState({relatedFiles: relatedFiles});
}).catch((error) => {
if (error.response.status === 500) {
this.setState({relatedFiles: []});
}
});
}
// update state // update state
this.setState({ this.setState({
isFileLoading: true, isFileLoading: true,
@@ -1151,6 +1174,34 @@ class LibContentView extends React.Component {
this.uploader.onFolderUpload(); this.uploader.onFolderUpload();
} }
onToolbarFileTagChanged = () => {
let repoID = this.props.repoID;
let filePath = this.state.path;
seafileAPI.listFileTags(repoID, filePath).then(res => {
let fileTags = res.data.file_tags.map(item => {
return new FileTag(item);
});
this.setState({fileTags: fileTags});
});
}
onToolbarRelatedFileChange = () => {
let repoID = this.props.repoID;
let filePath = this.state.path;
seafileAPI.listRelatedFiles(repoID, filePath).then(res => {
let relatedFiles = res.data.related_files.map((relatedFile) => {
return relatedFile;
});
this.setState({relatedFiles: relatedFiles});
}).catch((error) => {
if (error.response.status === 500) {
this.setState({relatedFiles: []});
}
});
}
render() { render() {
if (this.state.libNeedDecrypt) { if (this.state.libNeedDecrypt) {
@@ -1203,6 +1254,10 @@ class LibContentView extends React.Component {
filePermission={this.state.filePermission} filePermission={this.state.filePermission}
isDraft={this.state.isDraft} isDraft={this.state.isDraft}
hasDraft={this.state.hasDraft} hasDraft={this.state.hasDraft}
fileTags={this.state.fileTags}
relatedFiles={this.state.relatedFiles}
onFileTagChanged={this.onToolbarFileTagChanged}
onRelatedFileChange={this.onToolbarRelatedFileChange}
onSideNavMenuClick={this.props.onMenuClick} onSideNavMenuClick={this.props.onMenuClick}
repoID={this.props.repoID} repoID={this.props.repoID}
path={this.state.path} path={this.state.path}
@@ -1245,6 +1300,8 @@ class LibContentView extends React.Component {
hash={this.state.hash} hash={this.state.hash}
isDraft={this.state.isDraft} isDraft={this.state.isDraft}
hasDraft={this.state.hasDraft} hasDraft={this.state.hasDraft}
fileTags={this.state.fileTags}
relatedFiles={this.state.relatedFiles}
goDraftPage={this.goDraftPage} goDraftPage={this.goDraftPage}
isFileLoading={this.state.isFileLoading} isFileLoading={this.state.isFileLoading}
isFileLoadedErr={this.state.isFileLoadedErr} isFileLoadedErr={this.state.isFileLoadedErr}