1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-16 15:19:06 +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

@@ -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

@@ -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;

View File

@@ -0,0 +1,10 @@
.activity-table a {
color: #333;
}
.activity-op {
color: #707070;
background: #f0f0f0;
padding: 4px;
border-radius: 4px;
}

52
frontend/src/dashboard.js Normal file
View File

@@ -0,0 +1,52 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import SidePanel from './pages/dashboard/side-panel';
import MainPanel from './pages/dashboard/main-panel';
import Account from './components/account';
import Notification from './components/notification';
import { SeafileAPI } from './seafile-api';
import cookie from 'react-cookies';
import 'seafile-ui';
import './css/dashboard.css';
const siteRoot = window.app.config.siteRoot;
let seafileAPI = new SeafileAPI();
let xcsrfHeaders = cookie.load('csrftoken');
seafileAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders });
class DashBoard extends Component {
constructor(props) {
super(props);
this.state = {
isOpen: false
}
}
isOpen = () => {
this.setState({
isOpen: !this.state.isOpen,
})
}
render() {
return (
<div id="main">
<SidePanel isOpen={this.state.isOpen} toggleClose={this.isOpen} seafileAPI={seafileAPI}/>
<MainPanel isOpen={this.isOpen} seafileAPI={seafileAPI} >
<Notification seafileAPI={seafileAPI} />
<Account seafileAPI={seafileAPI}/>
</MainPanel>
</div>
)
}
}
ReactDOM.render(
<DashBoard />,
document.getElementById('wrapper')
);

View File

@@ -0,0 +1,30 @@
import React, { Component } from 'react';
import FilesActivities from '../../components/files-activities';
class MainPanel extends Component {
constructor(props) {
super(props);
}
onMenuClick = () => {
this.props.isOpen();
}
render() {
const { children } = this.props
return (
<div className="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="common-toolbar">
{children}
</div>
</div>
<FilesActivities seafileAPI={this.props.seafileAPI} />
</div>
)
}
}
export default MainPanel;

View File

@@ -0,0 +1,30 @@
import React, { Component } from 'react';
import SideNavFooter from '../../components/side-nav-footer';
import MainSideNav from '../../components/main-side-nav';
const siteRoot = window.app.config.siteRoot;
const serverRoot = window.app.config.serverRoot;
const logoPath = window.app.config.logoPath;
const mediaUrl = window.app.config.mediaUrl;
const siteTitle = window.app.config.siteTitle;
const logoWidth = window.app.config.logoWidth;
const logoHeight = window.app.config.logoHeight;
class SidePanel extends Component {
render() {
return (
<div className={`side-panel ${this.props.isOpen ? "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 href="#" title="Close" aria-label="Close" onClick={this.props.toggleClose} className="sf2-icon-x1 sf-popover-close side-panel-close op-icon d-md-none "></a>
</div>
<MainSideNav seafileAPI={this.props.seafileAPI}/>
<SideNavFooter />
</div>
)
}
}
export default SidePanel;

View File

@@ -1,8 +1,8 @@
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';
import Search from '../../components/search';
import MarkdownViewer from '../../components/markdown-viewer';
import Account from '../../components/account';
import { gettext, repoID, serviceUrl, slug, siteRoot } from '../../components/constance';
class MainPanel extends Component {

View File

@@ -1,10 +1,10 @@
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';
import TreeView from '../../components/tree-view/tree-view';
import { siteRoot, logoPath, mediaUrl, siteTitle, logoWidth, logoHeight } from '../../components/constance';
import Tree from '../../components/tree-view/tree';
import Node from '../../components/tree-view/node'
import NodeMenu from '../../components/menu-component/node-menu';
import MenuControl from '../../components/menu-component/node-menu-control';
const gettext = window.gettext;
class SidePanel extends Component {

View File

@@ -1,7 +1,7 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import SidePanel from './components/SidePanel';
import MainPanel from './components/MainPanel';
import SidePanel from './pages/wiki/side-panel';
import MainPanel from './pages/wiki/main-panel';
import moment from 'moment';
import cookie from 'react-cookies';
import { SeafileAPI } from 'seafile-js';