From 740d6a86cf8347c4a5320c869639934cdbd2da07 Mon Sep 17 00:00:00 2001 From: shanshuirenjia Date: Thu, 30 Aug 2018 15:10:52 +0800 Subject: [PATCH] React activity (#2315) --- frontend/src/components/Notification.js | 53 ++++ frontend/src/components/account.js | 3 +- frontend/src/components/files-activities.js | 243 ++++++++++++++++++ frontend/src/components/main-side-nav.js | 124 +++++++++ ...rchResultItem.js => search-result-item.js} | 0 frontend/src/components/search.js | 2 +- frontend/src/components/side-nav-footer.js | 54 ++++ frontend/src/css/dashboard.css | 10 + frontend/src/dashboard.js | 52 ++++ frontend/src/pages/dashboard/main-panel.js | 30 +++ frontend/src/pages/dashboard/side-panel.js | 30 +++ .../MainPanel.js => pages/wiki/main-panel.js} | 8 +- .../SidePanel.js => pages/wiki/side-panel.js} | 12 +- frontend/src/wiki.js | 4 +- media/css/seahub_react.css | 147 ++++++++++- seahub/templates/dashboard.html | 7 + seahub/urls.py | 1 + 17 files changed, 762 insertions(+), 18 deletions(-) create mode 100644 frontend/src/components/Notification.js create mode 100644 frontend/src/components/files-activities.js create mode 100644 frontend/src/components/main-side-nav.js rename frontend/src/components/{SearchResultItem.js => search-result-item.js} (100%) create mode 100644 frontend/src/components/side-nav-footer.js create mode 100644 frontend/src/css/dashboard.css create mode 100644 frontend/src/dashboard.js create mode 100644 frontend/src/pages/dashboard/main-panel.js create mode 100644 frontend/src/pages/dashboard/side-panel.js rename frontend/src/{components/MainPanel.js => pages/wiki/main-panel.js} (89%) rename frontend/src/{components/SidePanel.js => pages/wiki/side-panel.js} (95%) create mode 100644 seahub/templates/dashboard.html 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')}

+ +
+
+
    + {gettext('See All 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 + + +
    +

    logo

    +

    Server Version: 6.3.3
    © 2018 Seafile

    +

    About Us

    +
    +
    +
    +
    + ); + } +} + +class SideNavFooter extends React.Component { + render() { + return ( +
    + Help + + + {' '} + Clients + +
    + ); + } +} + +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 ( +
    +
    + +
    + {children} +
    +
    + +
    + ) + } +} + +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"),