diff --git a/frontend/src/components/Notification.js b/frontend/src/components/Notification.js
new file mode 100644
index 0000000000..52a7a120d8
--- /dev/null
+++ b/frontend/src/components/Notification.js
@@ -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 (
+
+
+
+ 0
+
+
+
+
+
{gettext('Notifications')}
+
+
+
+
+
+ )
+ }
+}
+
+export default Notification;
diff --git a/frontend/src/components/account.js b/frontend/src/components/account.js
index bca8548dfc..5ae77e1241 100644
--- a/frontend/src/components/account.js
+++ b/frontend/src/components/account.js
@@ -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) {
diff --git a/frontend/src/components/files-activities.js b/frontend/src/components/files-activities.js
new file mode 100644
index 0000000000..ef881e3cff
--- /dev/null
+++ b/frontend/src/components/files-activities.js
@@ -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 ;
+ } else if (error_msg) {
+ return {error_msg}
;
+ } else {
+ return (
+
+
+
+
+ {/* avatar */} |
+ {gettext("User")} |
+ {gettext("Operation")} |
+ {gettext("File")} / {gettext("Library")} |
+ {gettext("Time")} |
+
+
+
+
+ {events.has_more ? : ''}
+ {events.error_msg ? {events.error_msg}
: ''}
+
+ );
+ }
+ }
+}
+
+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 = {item.repo_name};
+ let smallLibLink = {item.repo_name};
+
+ if (item.obj_type == 'repo') {
+ switch(item.op_type) {
+ case 'create':
+ op = gettext("Created library");
+ details = {libLink} | ;
+ break;
+ case 'rename':
+ op = gettext("Renamed library");
+ details = {item.old_repo_name} => {libLink} | ;
+ break;
+ case 'delete':
+ op = gettext("Deleted library");
+ details = {item.repo_name} | ;
+ break;
+ case 'recover':
+ op = gettext("Restored library");
+ details = {libLink} | ;
+ 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 = {libLink} | ;
+ break;
+ }
+ } else if (item.obj_type == 'file') {
+ let fileURL = `${siteRoot}lib/${item.repo_id}/file${this.encodePath(item.path)}`;
+ let fileLink = {item.name};
+ switch(item.op_type) {
+ case 'create':
+ op = gettext("Created file");
+ details = {fileLink} {smallLibLink} | ;
+ break;
+ case 'delete':
+ op = gettext("Deleted file");
+ details = {item.name} {smallLibLink} | ;
+ break;
+ case 'recover':
+ op = gettext("Restored file");
+ details = {fileLink} {smallLibLink} | ;
+ break;
+ case 'rename':
+ op = gettext("Renamed file");
+ details = {item.old_name} => {fileLink} {smallLibLink} | ;
+ break;
+ case 'move':
+ let filePathLink = {item.path};
+ op = gettext("Moved file");
+ details = {item.old_path} => {filePathLink} {smallLibLink} | ;
+ break;
+ case 'edit': // update
+ op = gettext("Updated file");
+ details = {fileLink} {smallLibLink} | ;
+ break;
+ }
+ } else { // dir
+ let dirURL = `${siteRoot}#common/lib/${item.repo_id}${this.encodePath(item.path)}`;
+ let dirLink = {item.name};
+ switch(item.op_type) {
+ case 'create':
+ op = gettext("Created folder");
+ details = {dirLink} {smallLibLink} | ;
+ break;
+ case 'delete':
+ op = gettext("Deleted folder");
+ details = {item.name} {smallLibLink} | ;
+ break;
+ case 'recover':
+ op = gettext("Restored folder");
+ details = {dirLink} {smallLibLink} | ;
+ break;
+ case 'rename':
+ op = gettext("Renamed folder");
+ details = {item.old_name} => {dirLink} {smallLibLink} | ;
+ break;
+ case 'move':
+ let dirPathLink = {item.path};
+ op = gettext("Moved folder");
+ details = {item.old_path} => {dirPathLink} {smallLibLink} | ;
+ break;
+ }
+ }
+
+ return (
+
+
+
+ |
+
+ {item.author_name}
+ |
+ {op} |
+ {details}
+ |
+
+ );
+ }, this);
+
+ return (
+ {listFilesActivities}
+ );
+ }
+}
+
+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 (
+
+
+
{gettext("Activities")}
+
+
+
+
+
+ );
+ }
+}
+
+export default FilesActivities;
diff --git a/frontend/src/components/main-side-nav.js b/frontend/src/components/main-side-nav.js
new file mode 100644
index 0000000000..629f61c8d1
--- /dev/null
+++ b/frontend/src/components/main-side-nav.js
@@ -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 (
+
+ )
+ }
+
+ renderSharedAdmin() {
+ let height = 0;
+ if (this.state.sharedExtended) {
+ if (!this.adminHeight) {
+ this.adminHeight = 3 * this.listHeight;
+ }
+ height = this.adminHeight;
+ }
+ let style = {height: height};
+ return (
+
+ )
+ }
+
+ render() {
+ return (
+
+
+
Files
+
+
+
+
Tools
+
+
+
+
+ )
+ }
+}
+
+export default MainSideNav;
diff --git a/frontend/src/components/SearchResultItem.js b/frontend/src/components/search-result-item.js
similarity index 100%
rename from frontend/src/components/SearchResultItem.js
rename to frontend/src/components/search-result-item.js
diff --git a/frontend/src/components/search.js b/frontend/src/components/search.js
index b8251a3152..b54fc0d3d0 100644
--- a/frontend/src/components/search.js
+++ b/frontend/src/components/search.js
@@ -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 {
diff --git a/frontend/src/components/side-nav-footer.js b/frontend/src/components/side-nav-footer.js
new file mode 100644
index 0000000000..f46cbf44ee
--- /dev/null
+++ b/frontend/src/components/side-nav-footer.js
@@ -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 (
+
+
About
+
+
+
+

+
Server Version: 6.3.3
© 2018 Seafile
+
About Us
+
+
+
+
+ );
+ }
+}
+
+class SideNavFooter extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default SideNavFooter;
diff --git a/frontend/src/css/dashboard.css b/frontend/src/css/dashboard.css
new file mode 100644
index 0000000000..1fdf107e7f
--- /dev/null
+++ b/frontend/src/css/dashboard.css
@@ -0,0 +1,10 @@
+.activity-table a {
+ color: #333;
+}
+
+.activity-op {
+ color: #707070;
+ background: #f0f0f0;
+ padding: 4px;
+ border-radius: 4px;
+}
diff --git a/frontend/src/dashboard.js b/frontend/src/dashboard.js
new file mode 100644
index 0000000000..e9dc83c280
--- /dev/null
+++ b/frontend/src/dashboard.js
@@ -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 (
+
+ )
+ }
+}
+
+ReactDOM.render(
+ ,
+ document.getElementById('wrapper')
+);
diff --git a/frontend/src/pages/dashboard/main-panel.js b/frontend/src/pages/dashboard/main-panel.js
new file mode 100644
index 0000000000..e631553fb1
--- /dev/null
+++ b/frontend/src/pages/dashboard/main-panel.js
@@ -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 (
+
+ )
+ }
+}
+
+export default MainPanel;
diff --git a/frontend/src/pages/dashboard/side-panel.js b/frontend/src/pages/dashboard/side-panel.js
new file mode 100644
index 0000000000..404ec14ff3
--- /dev/null
+++ b/frontend/src/pages/dashboard/side-panel.js
@@ -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 (
+
+ )
+ }
+}
+export default SidePanel;
diff --git a/frontend/src/components/MainPanel.js b/frontend/src/pages/wiki/main-panel.js
similarity index 89%
rename from frontend/src/components/MainPanel.js
rename to frontend/src/pages/wiki/main-panel.js
index cc18f31e5c..b3adb1f74e 100644
--- a/frontend/src/components/MainPanel.js
+++ b/frontend/src/pages/wiki/main-panel.js
@@ -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 {
diff --git a/frontend/src/components/SidePanel.js b/frontend/src/pages/wiki/side-panel.js
similarity index 95%
rename from frontend/src/components/SidePanel.js
rename to frontend/src/pages/wiki/side-panel.js
index 7813daca40..7e15f7580f 100644
--- a/frontend/src/components/SidePanel.js
+++ b/frontend/src/pages/wiki/side-panel.js
@@ -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 {
diff --git a/frontend/src/wiki.js b/frontend/src/wiki.js
index 2c208b7f80..0b6bfea32c 100644
--- a/frontend/src/wiki.js
+++ b/frontend/src/wiki.js
@@ -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';
diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css
index e19312c192..b5a078fe8a 100644
--- a/media/css/seahub_react.css
+++ b/media/css/seahub_react.css
@@ -79,7 +79,10 @@ a:hover { color:#eb8205; }
.left-zero {
left: 0px !important;
}
-
+ul,ol,li {
+ padding:0;
+ margin:0;
+}
/* common elements */
@@ -261,6 +264,7 @@ a:hover { color:#eb8205; }
padding:12px 20px 16px;
background:#f8f8f8;
border-top:1px solid #eee;
+ border-right: 1px solid #eee;
}
.side-nav-footer .item {
color:#666;
@@ -572,6 +576,14 @@ a.op-icon:focus {
color:#999;
margin-right:15px;
}
+.side-panel-slide {
+ transition: all .3s ease-in-out;
+}
+
+.side-panel-slide-up {
+ transition: all .3s ease-in-out;
+ height: 0;
+}
#group-nav .sharp,
#share-admin-nav .sharp {
display:inline-block;
@@ -614,13 +626,33 @@ a.op-icon:focus {
text-decoration:none;
}
+.user-select-none {
+ -moz-user-select:none;
+ -webkit-user-select:none;
+ -ms-user-select:none;
+ -khtml-user-select:none;
+ user-select: none;
+}
+
.common-toolbar {
margin-left:auto;
display:flex;
}
#notifications {
position:relative;
- margin:5px 0 0 25px;
+ width: 32px;
+}
+#notifications .no-deco {
+ position: relative;
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+#notifications .sf2-icon-bell {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%,-50%);
}
@media (max-width: 390px) {
#notifications {
@@ -628,6 +660,13 @@ a.op-icon:focus {
}
}
+.sf-heading {
+ font-size: 1rem;
+ color: #322;
+ font-weight: normal;
+ line-height: 1.5;
+}
+
/* about dialog */
.about-content {
@@ -679,8 +718,6 @@ a.op-icon:focus {
padding:1px 30px 1px 5px;
}
-
-
.wiki-page-ops {
position:fixed;
top:10px;
@@ -726,3 +763,105 @@ select {
background:#fff;
line-height:17px;
}
+
+#notifications .sf2-icon-bell {
+ font-size:24px;
+ line-height:1;
+ color:#999;
+}
+#notifications .num {
+ position:absolute;
+ color:#fff;
+ font-size:12px;
+ line-height:1;
+ padding:1px 2px;
+ background:#feac74;
+ border:1px solid #cb8a5d;
+ top:0;
+ left:15px;
+}
+#notice-popover {
+ top:38px;
+ right:-12px;
+}
+#notice-popover .outer-caret {
+ right:18px;
+}
+#notice-popover a {
+ font-weight:normal;
+}
+#notice-popover li {
+ padding:9px 0 3px;
+ border-bottom:1px solid #dfdfe1;
+}
+#notice-popover li.unread {
+ background:#f5f5f7;
+ padding-right:10px;
+ padding-left:8px;
+ border-left:2px solid #feac74;
+ margin:0 -10px;
+}
+#notice-popover .avatar {
+ border-radius:1000px;
+ float:left;
+}
+#notice-popover .brief {
+ margin-left:40px;
+}
+#notice-popover .time {
+ color:#999;
+ text-align:right;
+ margin:0;
+ clear:both;
+}
+#notice-popover .view-all {
+ display:block;
+ padding:7px 0;
+ text-align:center;
+ color:#a4a4a4;
+}
+
+#notice-popover .sf-popover-close {
+ position: absolute;
+ right: 10px;
+ top: 5px;
+}
+
+
+/**** sf-popover ****/ /* e.g. top notice popup, group members popup */
+.sf-popover-container {
+ position:relative;
+}
+.sf-popover {
+ width:240px;
+ background:#fff;
+ border:1px solid #c9c9c9;
+ border-radius:3px;
+ box-shadow:0 0 4px #ccc;
+ position:absolute;
+ z-index: 20;
+}
+.sf-popover-hd {
+ padding:5px 0 3px;
+ border-bottom:1px solid #dfdfe1;
+ margin:0 10px;
+}
+.sf-popover-title {
+ text-align:center;
+}
+.sf-popover-close {
+ font-size:16px;
+ color:#b9b9b9;
+ margin:4px 0 0;
+}
+.sf-popover-con {
+ padding:0 10px;
+ overflow:auto;
+}
+
+.main-panel-main {
+ flex:auto;
+ display:flex;
+ flex-direction:column;
+ overflow:hidden; /* for ff */
+}
diff --git a/seahub/templates/dashboard.html b/seahub/templates/dashboard.html
new file mode 100644
index 0000000000..fad48d29a6
--- /dev/null
+++ b/seahub/templates/dashboard.html
@@ -0,0 +1,7 @@
+{% extends "base_for_react.html" %}
+{% load render_bundle from webpack_loader %}
+
+{% block extra_script %}
+{% render_bundle 'dashboard' %}
+{% endblock %}
+
diff --git a/seahub/urls.py b/seahub/urls.py
index 9c61cfa6b7..85ad8cb91f 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -178,6 +178,7 @@ urlpatterns = [
url(r'^modules/toggle/$', toggle_modules, name="toggle_modules"),
url(r'^download_client_program/$', TemplateView.as_view(template_name="download.html"), name="download_client"),
url(r'^choose_register/$', choose_register, name="choose_register"),
+ url(r'^dashboard/$', TemplateView.as_view(template_name="dashboard.html"), name="dashboard"),
### Ajax ###
url(r'^ajax/repo/(?P[-0-9a-f]{36})/dirents/$', get_dirents, name="get_dirents"),