1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-17 15:53:28 +00:00

React activity (#2315)

This commit is contained in:
shanshuirenjia
2018-08-30 15:10:52 +08:00
committed by Daniel Pan
parent 99080e95c3
commit 740d6a86cf
17 changed files with 762 additions and 18 deletions

View File

@@ -1,67 +0,0 @@
import React, { Component } from 'react';
import Search from './search';
import MarkdownViewer from './markdown-viewer';
import Account from './account';
import { gettext, repoID, serviceUrl, slug, siteRoot } from './constance';
class MainPanel extends Component {
onMenuClick = () => {
this.props.onMenuClick();
}
onEditClick = (e) => {
// const w=window.open('about:blank')
e.preventDefault();
window.location.href= serviceUrl + '/lib/' + repoID + '/file' + this.props.filePath + '?mode=edit';
}
render() {
var filePathList = this.props.filePath.split('/');
var pathElem = filePathList.map((item, index) => {
if (item == "") {
return;
} else {
return (
<span key={index}><span className="path-split">/</span>{item}</span>
)
}
});
return (
<div className="wiki-main-panel o-hidden">
<div className="main-panel-top panel-top">
<span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu" onClick={this.onMenuClick}></span>
<div className={`wiki-page-ops ${this.props.permission === 'rw' ? '' : 'hide'}`}>
<a className="btn btn-secondary btn-topbar" onClick={this.onEditClick}>{gettext("Edit Page")}</a>
</div>
<div className="common-toolbar">
<Search seafileAPI={this.props.seafileAPI} onSearchedClick={this.props.onSearchedClick}/>
<Account seafileAPI={this.props.seafileAPI} />
</div>
</div>
<div className="cur-view-main">
<div className="cur-view-path">
<div className="path-containter">
<a href={siteRoot + 'wikis/'} className="normal">{gettext("Wikis")}</a>
<span className="path-split">/</span>
<a href={siteRoot + 'wikis/' + slug} className="normal">{slug}</a>
{pathElem}
</div>
</div>
<div className="cur-view-container">
<MarkdownViewer
markdownContent={this.props.content}
latestContributor={this.props.latestContributor}
lastModified = {this.props.lastModified}
onLinkClick={this.props.onLinkClick}
isFileLoading={this.props.isFileLoading}
/>
</div>
</div>
</div>
)
}
}
export default MainPanel;

View File

@@ -0,0 +1,53 @@
import React from 'react';
const gettext = window.gettext;
class Notification extends React.Component {
constructor(props) {
super(props);
this.state = {
showNotice: false,
notice_html: ''
}
}
onClick = () => {
this.setState({
showNotice: !this.state.showNotice
})
if (!this.state.showNotice) {
this.loadNotices()
}
}
loadNotices = () => {
this.props.seafileAPI.getPopupNotices().then(res => {
this.setState({
notice_html: res.data.notice_html
})
})
}
render() {
return (
<div id="notifications">
<a href="#" onClick={this.onClick} className="no-deco" id="notice-icon" title="Notifications" aria-label="Notifications">
<span className="sf2-icon-bell"></span>
<span className="num hide">0</span>
</a>
<div id="notice-popover" className={`sf-popover ${this.state.showNotice ? '': 'hide'}`}>
<div className="outer-caret up-outer-caret"><div className="inner-caret"></div></div>
<div className="sf-popover-hd ovhd">
<h3 className="sf-popover-title">{gettext('Notifications')}</h3>
<a href="#" onClick={this.onClick} title={gettext('Close')} aria-label={gettext('Close')} className="sf-popover-close js-close sf2-icon-x1 op-icon float-right"></a>
</div>
<div className="sf-popover-con">
<ul className="notice-list" dangerouslySetInnerHTML={{__html: this.state.notice_html}}></ul>
<a href="/notification/list/" className="view-all">{gettext('See All Notifications')}</a>
</div>
</div>
</div>
)
}
}
export default Notification;

View File

@@ -1,267 +0,0 @@
import React, { Component } from 'react';
import TreeView from './tree-view/tree-view';
import { siteRoot, logoPath, mediaUrl, siteTitle, logoWidth, logoHeight } from './constance';
import Tree from './tree-view/tree';
import Node from './tree-view/node'
import NodeMenu from './menu-component/node-menu';
import MenuControl from './menu-component/node-menu-control';
const gettext = window.gettext;
class SidePanel extends Component {
constructor(props) {
super(props);
this.state = {
tree_data: new Tree(),
currentNode: null,
isNodeItemFrezee: false,
isShowMenu: false,
menuPosition: {
left: 0,
top: 0
},
isLoadFailed: false,
isMenuIconShow: false
}
}
closeSide = () => {
this.props.onCloseSide();
}
onMouseEnter = () => {
this.setState({
isMenuIconShow: true
})
}
onMouseLeave = () => {
this.setState({
isMenuIconShow: false
})
}
onNodeClick = (e, node) => {
this.setState({
currentNode: node
})
this.props.onFileClick(e, node)
}
onShowContextMenu = (e, node) => {
let left = e.clientX - 8*16;
let top = e.clientY + 10;
let position = Object.assign({},this.state.menuPosition, {left: left, top: top});
this.setState({
isShowMenu: !this.state.isShowMenu,
currentNode: node,
menuPosition: position,
isNodeItemFrezee: true
})
}
onHeadingMenuClick = (e) => {
e.nativeEvent.stopImmediatePropagation();
let node = this.state.tree_data.root;
let left = e.clientX - 8*16;
let top = e.clientY + 10;
let position = Object.assign({},this.state.menuPosition, {left: left, top: top});
this.setState({
isShowMenu: !this.state.isShowMenu,
currentNode: node,
menuPosition: position
})
}
onHideContextMenu = () => {
this.setState({
isShowMenu: false,
isNodeItemFrezee: false
})
}
onAddFolderNode = (dirPath) => {
this.props.editorUtilities.createDir(dirPath).then(res => {
this.initializeTreeData()
})
}
onAddFileNode = (filePath) => {
this.props.editorUtilities.createFile(filePath).then(res => {
this.initializeTreeData()
})
}
onRenameNode = (newName) => {
var node = this.state.currentNode;
let type = node.type;
let filePath = node.path;
if (type === 'file') {
this.props.editorUtilities.renameFile(filePath, newName).then(res => {
this.initializeTreeData()
if (this.isModifyCurrentFile()) {
node.name = newName;
this.props.onFileClick(null, node);
}
})
}
if (type === 'dir') {
this.props.editorUtilities.renameDir(filePath, newName).then(res => {
this.initializeTreeData();
if (this.isModifyContainsCurrentFile()) {
let currentNode = this.state.currentNode;
let nodePath = encodeURI(currentNode.path);
let pathname = window.location.pathname;
let start = pathname.indexOf(nodePath);
let node = currentNode.getNodeByPath(decodeURI(pathname.slice(start)));
if(node){
currentNode.name = newName;
this.props.onFileClick(null, node);
}
}
})
}
}
onDeleteNode = () => {
var currentNode = this.state.currentNode;
let filePath = currentNode.path;
let type = currentNode.type;
if (type === 'file') {
this.props.editorUtilities.deleteFile(filePath).then(res => {
this.initializeTreeData();
})
}
if (type === 'dir') {
this.props.editorUtilities.deleteDir(filePath).then(res => {
this.initializeTreeData();
})
}
let isCurrentFile = false;
if (this.state.currentNode.type === "dir") {
isCurrentFile = this.isModifyContainsCurrentFile();
} else {
isCurrentFile = this.isModifyCurrentFile();
}
if (isCurrentFile) {
let homeNode = this.getHomeNode();
this.props.onFileClick(null, homeNode);
}
}
isModifyCurrentFile() {
let name = this.state.currentNode.name;
let pathname = window.location.pathname;
let currentName = pathname.slice(pathname.lastIndexOf("/") + 1);
return name === currentName;
}
isModifyContainsCurrentFile() {
let pathname = window.location.pathname;
let nodePath = this.state.currentNode.path;
if (pathname.indexOf(nodePath)) {
return true;
}
return false;
}
initializeTreeData() {
this.props.editorUtilities.getFiles().then((files) => {
// construct the tree object
var rootObj = {
name: '/',
type: 'dir',
isExpanded: true
}
var treeData = new Tree();
treeData.parseFromList(rootObj, files);
let homeNode = this.getHomeNode(treeData);
this.setState({
tree_data: treeData,
currentNode: homeNode
})
}, () => {
console.log("failed to load files");
this.setState({
isLoadFailed: true
})
})
}
getHomeNode(treeData) {
let root = null;
if (treeData) {
root = treeData.root;
} else {
root = this.state.tree_data.root;
}
let homeNode = root.getNodeByPath(decodeURI("/home.md"));
return homeNode;
}
componentDidMount() {
//init treeview data
this.initializeTreeData();
document.addEventListener('click', this.onHideContextMenu);
}
componentWillUnmount() {
document.removeEventListener('click', this.onHideContextMenu);
}
render() {
return (
<div className={`wiki-side-panel ${this.props.closeSideBar ? "": "left-zero"}`}>
<div className="side-panel-top panel-top">
<a href={siteRoot} id="logo">
<img src={mediaUrl + logoPath} title={siteTitle} alt="logo" width={logoWidth} height={logoHeight} />
</a>
<a title="Close" aria-label="Close" onClick={this.closeSide} className="sf2-icon-x1 sf-popover-close side-panel-close op-icon d-md-none "></a>
</div>
<div id="side-nav" className="wiki-side-nav" role="navigation">
<h3
className="wiki-pages-heading"
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
{gettext("Pages")}
<div className="heading-icon">
<MenuControl
isShow={this.state.isMenuIconShow}
onClick={this.onHeadingMenuClick}
/>
</div>
</h3>
<div className="wiki-pages-container">
{this.state.tree_data &&
<TreeView
permission={this.props.permission}
treeData={this.state.tree_data}
currentNode={this.state.currentNode}
isNodeItemFrezee={this.state.isNodeItemFrezee}
onNodeClick={this.onNodeClick}
onShowContextMenu={this.onShowContextMenu}
/>
}
<NodeMenu
isShowMenu={this.state.isShowMenu}
menuPosition={this.state.menuPosition}
currentNode={this.state.currentNode}
onHideContextMenu={this.onHideContextMenu}
onDeleteNode={this.onDeleteNode}
onAddFileNode={this.onAddFileNode}
onAddFolderNode={this.onAddFolderNode}
onRenameNode={this.onRenameNode}
/>
</div>
</div>
</div>
)
}
}
export default SidePanel;

View File

@@ -2,8 +2,9 @@ import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import cookie from 'react-cookies';
import { keyCodes, bytesToSize } from './utils';
import { siteRoot, avatarInfo, gettext } from './constance';
const siteRoot = window.app.config.siteRoot;
const gettext = window.gettext;
class Account extends Component {
constructor(props) {

View File

@@ -0,0 +1,243 @@
import React, { Component } from 'react';
const gettext = window.gettext;
const siteRoot = window.app.config.siteRoot;
const per_page = 25; // default
class FileActivitiesContent extends Component {
render() {
const {loading, error_msg, events} = this.props.data;
if (loading) {
return <span className="loading-icon loading-tip"></span>;
} else if (error_msg) {
return <p className="error text-center">{error_msg}</p>;
} else {
return (
<React.Fragment>
<table className="table table-hover table-vcenter activity-table">
<thead>
<tr>
<th width="8%">{/* avatar */}</th>
<th width="10%">{gettext("User")}</th>
<th width="25%">{gettext("Operation")}</th>
<th width="37%">{gettext("File")} / {gettext("Library")}</th>
<th width="20%">{gettext("Time")}</th>
</tr>
</thead>
<TableBody items={events.items} />
</table>
{events.has_more ? <span className="loading-icon loading-tip"></span> : ''}
{events.error_msg ? <p className="error text-center">{events.error_msg}</p> : ''}
</React.Fragment>
);
}
}
}
class TableBody extends Component {
encodePath(path) {
let path_arr = path.split('/'),
path_arr_ = [];
for (let i = 0, len = path_arr.length; i < len; i++) {
path_arr_.push(encodeURIComponent(path_arr[i]));
}
return path_arr_.join('/');
}
render() {
let listFilesActivities = this.props.items.map(function(item, index) {
let op, details;
let userProfileURL = `${siteRoot}profile/${encodeURIComponent(item.author_email)}/`;
let libURL = `${siteRoot}#common/lib/${item.repo_id}`;
let libLink = <a href={libURL}>{item.repo_name}</a>;
let smallLibLink = <a className="small text-secondary" href={libURL}>{item.repo_name}</a>;
if (item.obj_type == 'repo') {
switch(item.op_type) {
case 'create':
op = gettext("Created library");
details = <td>{libLink}</td>;
break;
case 'rename':
op = gettext("Renamed library");
details = <td>{item.old_repo_name} => {libLink}</td>;
break;
case 'delete':
op = gettext("Deleted library");
details = <td>{item.repo_name}</td>;
break;
case 'recover':
op = gettext("Restored library");
details = <td>{libLink}</td>;
break;
case 'clean-up-trash':
if (item.days == 0) {
op = gettext("Removed all items from trash.");
} else {
op = gettext("Removed items older than {n} days from trash.").replace('{n}', item.days);
}
details = <td>{libLink}</td>;
break;
}
} else if (item.obj_type == 'file') {
let fileURL = `${siteRoot}lib/${item.repo_id}/file${this.encodePath(item.path)}`;
let fileLink = <a href={fileURL}>{item.name}</a>;
switch(item.op_type) {
case 'create':
op = gettext("Created file");
details = <td>{fileLink}<br />{smallLibLink}</td>;
break;
case 'delete':
op = gettext("Deleted file");
details = <td>{item.name}<br />{smallLibLink}</td>;
break;
case 'recover':
op = gettext("Restored file");
details = <td>{fileLink}<br />{smallLibLink}</td>;
break;
case 'rename':
op = gettext("Renamed file");
details = <td>{item.old_name} => {fileLink}<br />{smallLibLink}</td>;
break;
case 'move':
let filePathLink = <a href={fileURL}>{item.path}</a>;
op = gettext("Moved file");
details = <td>{item.old_path} => {filePathLink}<br />{smallLibLink}</td>;
break;
case 'edit': // update
op = gettext("Updated file");
details = <td>{fileLink}<br />{smallLibLink}</td>;
break;
}
} else { // dir
let dirURL = `${siteRoot}#common/lib/${item.repo_id}${this.encodePath(item.path)}`;
let dirLink = <a href={dirURL}>{item.name}</a>;
switch(item.op_type) {
case 'create':
op = gettext("Created folder");
details = <td>{dirLink}<br />{smallLibLink}</td>;
break;
case 'delete':
op = gettext("Deleted folder");
details = <td>{item.name}<br />{smallLibLink}</td>;
break;
case 'recover':
op = gettext("Restored folder");
details = <td>{dirLink}<br />{smallLibLink}</td>;
break;
case 'rename':
op = gettext("Renamed folder");
details = <td>{item.old_name} => {dirLink}<br />{smallLibLink}</td>;
break;
case 'move':
let dirPathLink = <a href={dirURL}>{item.path}</a>;
op = gettext("Moved folder");
details = <td>{item.old_path} => {dirPathLink}<br />{smallLibLink}</td>;
break;
}
}
return (
<tr key={index}>
<td className="text-center">
<img src={item.avatar_url} alt="" width="24" className="avatar" />
</td>
<td>
<a href={userProfileURL}>{item.author_name}</a>
</td>
<td><span className="activity-op">{op}</span></td>
{details}
<td className="text-secondary" dangerouslySetInnerHTML={{__html:item.time_relative}}></td>
</tr>
);
}, this);
return (
<tbody>{listFilesActivities}</tbody>
);
}
}
class FilesActivities extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
error_msg: '',
events: {}
};
this.handleScroll = this.handleScroll.bind(this);
}
componentDidMount() {
const pageNum = 1
this.props.seafileAPI.getActivities(pageNum)
.then(res => {
// not logged in
if (res.status == 403) {
this.setState({
loading: false,
error_msg: gettext("Permission denied")
});
} else {
// {"events":[...]}
this.setState({
loading: false,
events: {
page: 1,
items: res.data.events,
has_more: res.data.events.length == per_page ? true : false
}
});
}
});
}
getMore() {
const pageNum = this.state.events.page + 1;
this.props.seafileAPI.getActivities(pageNum)
.then(res => {
this.setState(function(prevState, props) {
let events = prevState.events;
if (res.status == 403) { // log out
events.error_msg = gettext("Permission denied");
events.has_more = false;
}
if (res.ok) {
events.page += 1;
events.items = events.items.concat(res.data.events);
events.has_more = res.data.events.length == per_page ? true : false;
}
return {events: events};
});
});
}
handleScroll(e) {
let $el = e.target;
if (this.state.events.has_more &&
$el.scrollTop > 0 &&
$el.clientHeight + $el.scrollTop == $el.scrollHeight) { // scroll to the bottom
this.getMore();
}
}
render() {
return (
<div className="main-panel-main" id="activities">
<div className="cur-view-path">
<h3 className="sf-heading">{gettext("Activities")}</h3>
</div>
<div className="cur-view-main-con" onScroll={this.handleScroll}>
<FileActivitiesContent data={this.state} />
</div>
</div>
);
}
}
export default FilesActivities;

View File

@@ -0,0 +1,124 @@
import React from 'react';
const siteRoot = window.app.config.siteRoot;
const serverRoot = window.app.config.serverRoot;
class MainSideNav extends React.Component {
constructor(props) {
super(props);
this.state = {
groupsExtended: false,
sharedExtended: false,
closeSideBar:false,
groupItems: []
};
this.listHeight = 24; //for caculate tabheight
this.groupsHeight = 0;
this.adminHeight = 0;
}
grpsExtend = () => {
this.setState({
groupsExtended: !this.state.groupsExtended,
})
this.loadGroups();
}
shExtend = () => {
this.setState({
sharedExtended: !this.state.sharedExtended,
})
}
loadGroups = () => {
let _this = this;
this.props.seafileAPI.getGroups().then(res =>{
let data = res.data.groups;
this.groupsHeight = (data.length + 1) * _this.listHeight;
_this.setState({
groupItems: data
})
})
}
renderSharedGroups() {
let style = {height: 0};
if (this.state.groupsExtended) {
style = {height: this.groupsHeight};
}
return (
<ul className={`grp-list ${this.state.groupsExtended ? 'side-panel-slide' : 'side-panel-slide-up'}`} style={style}>
<li>
<a href={siteRoot + '#groups/'}>
<span className="sharp" aria-hidden="true">#</span>All Groups</a>
</li>
{this.state.groupItems.map(item => {
return (
<li key={item.id}>
<a href={siteRoot + '#group/' + item.id + '/'}>
<span className="sharp" aria-hidden="true">#</span>{item.name}</a>
</li>
)
})}
</ul>
)
}
renderSharedAdmin() {
let height = 0;
if (this.state.sharedExtended) {
if (!this.adminHeight) {
this.adminHeight = 3 * this.listHeight;
}
height = this.adminHeight;
}
let style = {height: height};
return (
<ul className={`${this.state.sharedExtended ? 'side-panel-slide' : 'side-panel-slide-up'}`} style={style} >
<li>
<a href={siteRoot + '#share-admin-libs/'}><span aria-hidden="true" className="sharp">#</span>Libraries</a>
</li>
<li>
<a href={siteRoot + '#share-admin-folders/'}><span aria-hidden="true" className="sharp">#</span>Folders</a>
</li>
<li>
<a href={siteRoot + '#share-admin-share-links/'}><span aria-hidden="true" className="sharp">#</span>Links</a>
</li>
</ul>
)
}
render() {
return (
<div id="side-nav" className="home-side-nav">
<div className="side-nav-con">
<h3 className="sf-heading">Files</h3>
<ul className="side-tabnav-tabs">
<li className="tab"><a href={siteRoot + '#my-libs'} className="ellipsis" title="My Libraries"><span className="sf2-icon-user" aria-hidden="true"></span>My Libraries</a></li>
<li className="tab"><a href={serverRoot + siteRoot + '#shared-libs/'} className="ellipsis" title="Shared with me"><span className="sf2-icon-share" aria-hidden="true"></span>Shared with me</a></li>
<li className="tab"><a href={serverRoot + siteRoot + '#org/'} className="ellipsis" title="Shared with all"><span className="sf2-icon-organization" aria-hidden="true"></span>Shared with all</a></li>
<li className="tab" id="group-nav">
<a className="ellipsis user-select-no" title="Shared with groups" onClick={this.grpsExtend}><span className={`toggle-icon float-right ${this.state.groupsExtended ?'icon-caret-down':'icon-caret-left'}`} aria-hidden="true"></span><span className="sf2-icon-group" aria-hidden="true"></span>Shared with groups</a>
{this.renderSharedGroups()}
</li>
</ul>
<div className="hd w-100 o-hidden">
<h3 className="float-left sf-heading">Tools</h3>
</div>
<ul className="side-tabnav-tabs">
<li className="tab"><a href={siteRoot + '#starred/'}><span className="sf2-icon-star" aria-hidden="true"></span>Favorites</a></li>
<li className="tab"><a href={siteRoot + 'dashboard'}><span className="sf2-icon-clock" aria-hidden="true"></span>Acitivities</a></li>
<li className="tab"><a href={siteRoot + '#devices/'} className="ellipsis" title="Linked Devices"><span className="sf2-icon-monitor" aria-hidden="true"></span>Linked Devices</a></li>
<li className="tab" id="share-admin-nav">
<a className="ellipsis user-select-no" title="Share Admin" onClick={this.shExtend}><span className={`toggle-icon float-right ${this.state.sharedExtended ? 'icon-caret-down':'icon-caret-left'}`} aria-hidden="true"></span><span aria-hidden="true" className="sf2-icon-wrench"></span>Share Admin</a>
{this.renderSharedAdmin()}
</li>
</ul>
</div>
</div>
)
}
}
export default MainSideNav;

View File

@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { gettext, repoID } from './constance';
import SearchResultItem from './SearchResultItem';
import SearchResultItem from './search-result-item';
class Search extends Component {

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { Modal, ModalHeader, ModalBody } from 'reactstrap';
class About extends React.Component {
constructor(props) {
super(props);
this.state = {
modal: false
};
this.toggle = this.toggle.bind(this);
}
toggle() {
this.setState({
modal: !this.state.modal
});
}
render() {
return (
<div>
<a href="#" className="item" onClick={this.toggle}>About</a>
<Modal isOpen={this.state.modal} toggle={this.toggle} className={this.props.className}>
<ModalBody>
<div className="about-content">
<p><img src="/media/img/seafile-logo.png" title="Private Seafile" alt="logo" width="128" height="32" /></p>
<p>Server Version: 6.3.3<br /> © 2018 Seafile</p>
<p><a href="http://seafile.com/about/" target="_blank">About Us</a></p>
</div>
</ModalBody>
</Modal>
</div>
);
}
}
class SideNavFooter extends React.Component {
render() {
return (
<div className="side-nav-footer">
<a href="/help/" target="_blank" className="item">Help</a>
<About />
<a href="/download_client_program/" className="item last-item">
<span aria-hidden="true" className="sf2-icon-monitor vam"></span>{' '}
<span className="vam">Clients</span>
</a>
</div>
);
}
}
export default SideNavFooter;