diff --git a/frontend/src/app.js b/frontend/src/app.js index 7e8f934858..3a2cbc2e88 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -11,6 +11,7 @@ import MainPanel from './components/main-panel'; import DraftsView from './pages/drafts/drafts-view'; import DraftContent from './pages/drafts/draft-content'; import FilesActivities from './pages/dashboard/files-activities'; +import MyFileActivities from './pages/dashboard/my-file-activities'; import Starred from './pages/starred/starred'; import LinkedDevices from './pages/linked-devices/linked-devices'; import editUtilities from './utils/editor-utilities'; @@ -37,6 +38,7 @@ import './css/toolbar.css'; import './css/search.css'; const FilesActivitiesWrapper = MainContentWrapper(FilesActivities); +const MyFileActivitiesWrapper = MainContentWrapper(MyFileActivities); const DraftsViewWrapper = MainContentWrapper(DraftsView); const StarredWrapper = MainContentWrapper(Starred); const LinkedDevicesWrapper = MainContentWrapper(LinkedDevices); @@ -241,6 +243,7 @@ class App extends Component { {home} + { + this.setState({ + isListCreatedFiles: !this.state.isListCreatedFiles, + }); + }; + + render() { + const isDesktop = this.props.isDesktop; + let {item, index, items} = this.props; + let op, details, moreDetails = false; + let userProfileURL = `${siteRoot}profile/${encodeURIComponent(item.author_email)}/`; + + let libURL = siteRoot + 'library/' + item.repo_id + '/' + encodeURIComponent(item.repo_name) + '/'; + 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': + op = gettext('Cleaned trash'); + if (item.days == 0) { + details = gettext('Removed all items from trash.'); + } else { + details = gettext('Removed items older than {n} days from trash.').replace('{n}', item.days); + } + moreDetails = true; + break; + } + } else if (item.obj_type == 'draft') { + let fileURL = `${siteRoot}lib/${item.repo_id}/file${Utils.encodePath(item.path)}`; + let fileLink = {item.name}; + op = gettext('Publish draft'); + details = fileLink; + moreDetails = true; + } else if (item.obj_type == 'files') { + let fileURL = `${siteRoot}lib/${item.repo_id}/file${Utils.encodePath(item.path)}`; + if (item.name.endsWith('(draft).md')) { + fileURL = serviceURL + '/drafts/' + item.draft_id + '/'; + } + let fileLink = `${item.name}`; + if (item.name.endsWith('(draft).md') && !item.draft_id) { + fileLink = item.name; + } + let fileCount = item.createdFilesCount - 1; + let firstLine = gettext('{file} and {n} other files') + .replace('{file}', fileLink) + .replace('{n}', fileCount); + op = gettext('Created {n} files').replace('{n}', item.createdFilesCount); + details = ( + +

+ {isDesktop && } +
+ ); + moreDetails = true; + } else if (item.obj_type == 'file') { + const isDraft = item.name.endsWith('(draft).md'); + const fileURL = isDraft ? serviceURL + '/drafts/' + item.draft_id + '/' : + `${siteRoot}lib/${item.repo_id}/file${Utils.encodePath(item.path)}`; + let fileLink = {item.name}; + if (isDraft && !item.draft_id) { + fileLink = item.name; + } + switch (item.op_type) { + case 'create': + op = isDraft ? gettext('Created draft') : gettext('Created file'); + details = fileLink; + moreDetails = true; + break; + case 'delete': + op = isDraft ? gettext('Deleted draft') : gettext('Deleted file'); + details = item.name; + moreDetails = true; + break; + case 'recover': + op = gettext('Restored file'); + details = fileLink; + moreDetails = true; + break; + case 'rename': + op = gettext('Renamed file'); + details = {item.old_name} => {fileLink}; + moreDetails = true; + break; + case 'move': + // eslint-disable-next-line + const filePathLink = {item.path}; + op = gettext('Moved file'); + details = {item.old_path} => {filePathLink}; + moreDetails = true; + break; + case 'edit': // update + op = isDraft ? gettext('Updated draft') : gettext('Updated file'); + details = fileLink; + moreDetails = true; + break; + } + } else { // dir + let dirURL = siteRoot + 'library/' + item.repo_id + '/' + encodeURIComponent(item.repo_name) + Utils.encodePath(item.path); + let dirLink = {item.name}; + switch (item.op_type) { + case 'create': + op = gettext('Created folder'); + details = dirLink; + moreDetails = true; + break; + case 'delete': + op = gettext('Deleted folder'); + details = item.name; + moreDetails = true; + break; + case 'recover': + op = gettext('Restored folder'); + details = dirLink; + moreDetails = true; + break; + case 'rename': + op = gettext('Renamed folder'); + details = {item.old_name} => {dirLink}; + moreDetails = true; + break; + case 'move': + // eslint-disable-next-line + const dirPathLink = {item.path}; + op = gettext('Moved folder'); + details = {item.old_path} => {dirPathLink}; + moreDetails = true; + break; + } + } + + let isShowDate = true; + if (index > 0) { + let lastEventTime = items[index - 1].time; + isShowDate = moment(item.time).isSame(lastEventTime, 'day') ? false : true; + } + + return ( + + {isShowDate && + + {moment(item.time).format('YYYY-MM-DD')} + + } + {isDesktop ? ( + + + + + + {item.author_name} + + {op} + + {details} + {moreDetails &&
} + {moreDetails && smallLibLink} + + + + + + ) : ( + + + + + + {item.author_name} +

{op}

+ {details} + + + + + + {moreDetails &&
} + {moreDetails && libLink} + + + )} + {this.state.isListCreatedFiles && + + + + } +
+ ); + } +} + +ActivityItem.propTypes = activityPropTypes; + +export default ActivityItem; diff --git a/frontend/src/pages/dashboard/content.js b/frontend/src/pages/dashboard/content.js new file mode 100644 index 0000000000..f0ed13d124 --- /dev/null +++ b/frontend/src/pages/dashboard/content.js @@ -0,0 +1,76 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { gettext } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; +import EmptyTip from '../../components/empty-tip'; +import ActivityItem from './activity-item'; + +import '../../css/files-activities.css'; + +moment.locale(window.app.config.lang); + +const contentPropTypes = { + isLoadingMore: PropTypes.bool.isRequired, + items: PropTypes.array.isRequired, +}; + +class FileActivitiesContent extends Component { + + render() { + const isDesktop = Utils.isDesktop(); + let { items, isLoadingMore } = this.props; + + if (!items.length) { + return

{gettext('No more activities')}

; + } + + const desktopThead = ( + + + {/* avatar */} + {gettext('User')} + {gettext('Operation')} + {gettext('File')} / {gettext('Library')} + {gettext('Time')} + + + ); + + const mobileThead = ( + + + + + + + + ); + + return ( + + + {isDesktop ? desktopThead : mobileThead} + + {items.map((item, index) => { + return ( + + ); + })} + +
+ {isLoadingMore ? : ''} +
+ ); + } +} + +FileActivitiesContent.propTypes = contentPropTypes; + +export default FileActivitiesContent; diff --git a/frontend/src/pages/dashboard/files-activities.js b/frontend/src/pages/dashboard/files-activities.js index 426eb290a3..36bf9876fc 100644 --- a/frontend/src/pages/dashboard/files-activities.js +++ b/frontend/src/pages/dashboard/files-activities.js @@ -1,306 +1,25 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; +import { Link } from '@gatsbyjs/reach-router'; import { seafileAPI } from '../../utils/seafile-api'; -import { gettext, siteRoot, serviceURL } from '../../utils/constants'; +import { gettext, siteRoot, username } from '../../utils/constants'; import { Utils } from '../../utils/utils'; import Loading from '../../components/loading'; import Activity from '../../models/activity'; -import ListCreatedFileDialog from '../../components/dialog/list-created-files-dialog'; -import ModalPortal from '../../components/modal-portal'; +import FileActivitiesContent from './content'; +import UserSelector from './user-selector'; import '../../css/files-activities.css'; moment.locale(window.app.config.lang); -const contentPropTypes = { - isLoadingMore: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired, +const propTypes = { + onlyMine: PropTypes.bool }; -class FileActivitiesContent extends Component { - - render() { - const isDesktop = Utils.isDesktop(); - let { items, isLoadingMore } = this.props; - - const desktopThead = ( - - - {/* avatar */} - {gettext('User')} - {gettext('Operation')} - {gettext('File')} / {gettext('Library')} - {gettext('Time')} - - - ); - - const mobileThead = ( - - - - - - - - ); - - return ( - - - {isDesktop ? desktopThead : mobileThead} - - {items.map((item, index) => { - return ( - - ); - })} - -
- {isLoadingMore ? : ''} -
- ); - } -} - -FileActivitiesContent.propTypes = contentPropTypes; - -const activityPropTypes = { - item: PropTypes.object.isRequired, - index: PropTypes.number.isRequired, - items: PropTypes.array.isRequired, - isDesktop: PropTypes.bool.isRequired, -}; - -class ActivityItem extends Component { - - constructor(props) { - super(props); - this.state = { - isListCreatedFiles: false, - }; - } - - onListCreatedFilesToggle = () => { - this.setState({ - isListCreatedFiles: !this.state.isListCreatedFiles, - }); - }; - - render() { - const isDesktop = this.props.isDesktop; - let {item, index, items} = this.props; - let op, details, moreDetails = false; - let userProfileURL = `${siteRoot}profile/${encodeURIComponent(item.author_email)}/`; - - let libURL = siteRoot + 'library/' + item.repo_id + '/' + encodeURIComponent(item.repo_name) + '/'; - 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': - op = gettext('Cleaned trash'); - if (item.days == 0) { - details = gettext('Removed all items from trash.'); - } else { - details = gettext('Removed items older than {n} days from trash.').replace('{n}', item.days); - } - moreDetails = true; - break; - } - } else if (item.obj_type == 'draft') { - let fileURL = `${siteRoot}lib/${item.repo_id}/file${Utils.encodePath(item.path)}`; - let fileLink = {item.name}; - op = gettext('Publish draft'); - details = fileLink; - moreDetails = true; - } else if (item.obj_type == 'files') { - let fileURL = `${siteRoot}lib/${item.repo_id}/file${Utils.encodePath(item.path)}`; - if (item.name.endsWith('(draft).md')) { - fileURL = serviceURL + '/drafts/' + item.draft_id + '/'; - } - let fileLink = `${item.name}`; - if (item.name.endsWith('(draft).md') && !item.draft_id) { - fileLink = item.name; - } - let fileCount = item.createdFilesCount - 1; - let firstLine = gettext('{file} and {n} other files') - .replace('{file}', fileLink) - .replace('{n}', fileCount); - op = gettext('Created {n} files').replace('{n}', item.createdFilesCount); - details = ( - -

- {isDesktop && } -
- ); - moreDetails = true; - } else if (item.obj_type == 'file') { - const isDraft = item.name.endsWith('(draft).md'); - const fileURL = isDraft ? serviceURL + '/drafts/' + item.draft_id + '/' : - `${siteRoot}lib/${item.repo_id}/file${Utils.encodePath(item.path)}`; - let fileLink = {item.name}; - if (isDraft && !item.draft_id) { - fileLink = item.name; - } - switch (item.op_type) { - case 'create': - op = isDraft ? gettext('Created draft') : gettext('Created file'); - details = fileLink; - moreDetails = true; - break; - case 'delete': - op = isDraft ? gettext('Deleted draft') : gettext('Deleted file'); - details = item.name; - moreDetails = true; - break; - case 'recover': - op = gettext('Restored file'); - details = fileLink; - moreDetails = true; - break; - case 'rename': - op = gettext('Renamed file'); - details = {item.old_name} => {fileLink}; - moreDetails = true; - break; - case 'move': - // eslint-disable-next-line - const filePathLink = {item.path}; - op = gettext('Moved file'); - details = {item.old_path} => {filePathLink}; - moreDetails = true; - break; - case 'edit': // update - op = isDraft ? gettext('Updated draft') : gettext('Updated file'); - details = fileLink; - moreDetails = true; - break; - } - } else { // dir - let dirURL = siteRoot + 'library/' + item.repo_id + '/' + encodeURIComponent(item.repo_name) + Utils.encodePath(item.path); - let dirLink = {item.name}; - switch (item.op_type) { - case 'create': - op = gettext('Created folder'); - details = dirLink; - moreDetails = true; - break; - case 'delete': - op = gettext('Deleted folder'); - details = item.name; - moreDetails = true; - break; - case 'recover': - op = gettext('Restored folder'); - details = dirLink; - moreDetails = true; - break; - case 'rename': - op = gettext('Renamed folder'); - details = {item.old_name} => {dirLink}; - moreDetails = true; - break; - case 'move': - // eslint-disable-next-line - const dirPathLink = {item.path}; - op = gettext('Moved folder'); - details = {item.old_path} => {dirPathLink}; - moreDetails = true; - break; - } - } - - let isShowDate = true; - if (index > 0) { - let lastEventTime = items[index - 1].time; - isShowDate = moment(item.time).isSame(lastEventTime, 'day') ? false : true; - } - - return ( - - {isShowDate && - - {moment(item.time).format('YYYY-MM-DD')} - - } - {isDesktop ? ( - - - - - - {item.author_name} - - {op} - - {details} - {moreDetails &&
} - {moreDetails && smallLibLink} - - - - - - ) : ( - - - - - - {item.author_name} -

{op}

- {details} - - - - - - {moreDetails &&
} - {moreDetails && libLink} - - - )} - {this.state.isListCreatedFiles && - - - - } -
- ); - } -} - -ActivityItem.propTypes = activityPropTypes; - class FilesActivities extends Component { + constructor(props) { super(props); this.state = { @@ -309,21 +28,37 @@ class FilesActivities extends Component { isLoadingMore: false, currentPage: 1, hasMore: true, + allItems: [], items: [], + availableUsers: [], + targetUsers: [] }; this.avatarSize = 72; this.curPathList = []; this.oldPathList = []; + this.availableUserEmails = new Set(); } componentDidMount() { - let currentPage = this.state.currentPage; + let { currentPage, availableUsers } = this.state; seafileAPI.listActivities(currentPage, this.avatarSize).then(res => { // {"events":[...]} let events = this.mergePublishEvents(res.data.events); events = this.mergeFileCreateEvents(events); + events.forEach(item => { + if (!this.availableUserEmails.has(item.author_email)) { + this.availableUserEmails.add(item.author_email); + availableUsers.push({ + email: item.author_email, + name: item.author_name, + avatar_url: item.avatar_url + }); + } + }); this.setState({ - items: events, + allItems: events, + items: this.filterEvents(events), + availableUsers: availableUsers, currentPage: currentPage + 1, isFirstLoading: false, hasMore: true, @@ -400,19 +135,34 @@ class FilesActivities extends Component { }; getMore() { - let currentPage = this.state.currentPage; + const { currentPage, availableUsers, targetUsers } = this.state; seafileAPI.listActivities(currentPage, this.avatarSize).then(res => { // {"events":[...]} let events = this.mergePublishEvents(res.data.events); events = this.mergeFileCreateEvents(events); + events.forEach(item => { + if (!this.availableUserEmails.has(item.author_email)) { + this.availableUserEmails.add(item.author_email); + availableUsers.push({ + email: item.author_email, + name: item.author_name, + avatar_url: item.avatar_url + }); + } + }); + const filteredEvents = this.filterEvents(events); this.setState({ - isLoadingMore: false, - items: [...this.state.items, ...events], + allItems: [...this.state.allItems, ...events], + items: [...this.state.items, ...filteredEvents], + availableUsers: availableUsers, currentPage: currentPage + 1, + isLoadingMore: false, hasMore: res.data.events.length === 0 ? false : true }); if (this.state.items.length < 25 && this.state.hasMore) { - this.getMore(); + if (!(targetUsers.length && currentPage == 100)) { + this.getMore(); + } } }).catch(error => { this.setState({ @@ -422,6 +172,34 @@ class FilesActivities extends Component { }); } + filterEvents = (events) => { + const { onlyMine } = this.props; + const { targetUsers } = this.state; + + if (onlyMine) { + return events.filter(item => item.author_email == username); + } else if (targetUsers.length) { + return events.filter(item => targetUsers.map(item => item.email).indexOf(item.author_email) != -1); + } else { + return events; + } + }; + + setTargetUsers = (selectedUsers) => { + this.setState({ + targetUsers: selectedUsers + }, () => { + const items = this.filterEvents(this.state.allItems); + this.setState({ + items: items + }, () => { + if (items.length < 25 && this.state.hasMore) { + this.getMore(); + } + }); + }); + }; + handleScroll = (event) => { if (!this.state.isLoadingMore && this.state.hasMore) { const clientHeight = event.target.clientHeight; @@ -437,19 +215,38 @@ class FilesActivities extends Component { }; render() { + const { onlyMine } = this.props; + const { targetUsers, availableUsers } = this.state; return (
-

{gettext('Activities')}

+
    +
  • + {gettext('All Activities')} +
  • +
  • + {gettext('My Activities')} +
  • +
{this.state.isFirstLoading && } {(!this.state.isFirstLoading && this.state.errorMsg) &&

{this.state.errorMsg}

} - {!this.state.isFirstLoading && - + {!this.state.isFirstLoading && ( + + {!onlyMine && ( + + )} + + + ) }
@@ -458,4 +255,6 @@ class FilesActivities extends Component { } } +FilesActivities.propTypes = propTypes; + export default FilesActivities; diff --git a/frontend/src/pages/dashboard/my-file-activities.js b/frontend/src/pages/dashboard/my-file-activities.js new file mode 100644 index 0000000000..483030593c --- /dev/null +++ b/frontend/src/pages/dashboard/my-file-activities.js @@ -0,0 +1,11 @@ +import React, { Component } from 'react'; +import FilesActivities from './files-activities'; + +class MyFilesActivities extends Component { + + render() { + return ; + } +} + +export default MyFilesActivities; diff --git a/frontend/src/pages/dashboard/user-selector.js b/frontend/src/pages/dashboard/user-selector.js new file mode 100644 index 0000000000..ebf7088125 --- /dev/null +++ b/frontend/src/pages/dashboard/user-selector.js @@ -0,0 +1,122 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Input } from 'reactstrap'; +import { gettext } from '../../utils/constants'; + +import '../../css/files-activities.css'; + +const propTypes = { + availableUsers: PropTypes.array.isRequired, + currentSelectedUsers: PropTypes.array.isRequired, + setTargetUsers: PropTypes.func.isRequired, +}; + +class UserSelector extends Component { + + constructor(props) { + super(props); + this.state = { + isPopoverOpen: false, + availableUsers: props.availableUsers.map(item => { + item.isSelected = false; + return item; + }), + filteredAvailableUsers: props.availableUsers.map(item => { + item.isSelected = false; + return item; + }) + }; + } + + togglePopover = () => { + this.setState({ + isPopoverOpen: !this.state.isPopoverOpen + }, () => { + if (!this.state.isPopoverOpen) { + const { availableUsers } = this.state; + const selectedUsers = availableUsers.filter(item => item.isSelected); + this.props.setTargetUsers(selectedUsers); + } + }); + }; + + searchUsers = (e) => { + const { availableUsers } = this.state; + const query = e.target.value.trim(); + const filteredAvailableUsers = availableUsers.filter(item => item.email.indexOf(query) != -1); + this.setState({ + filteredAvailableUsers: filteredAvailableUsers + }); + }; + + toggleSelectItem = (targetItem) => { + const { availableUsers } = this.state; + const handleItem = (item) => { + if (item.email == targetItem.email) { + item.isSelected = !targetItem.isSelected; + } + return item; + }; + + this.setState({ + availableUsers: availableUsers.map(handleItem), + }); + }; + + render() { + const { isPopoverOpen, availableUsers, filteredAvailableUsers } = this.state; + const { currentSelectedUsers } = this.props; + const selectedUsers = availableUsers.filter(item => item.isSelected); + return ( +
+ + {gettext('Modified by:')} + {currentSelectedUsers.length > 0 && ( + {currentSelectedUsers.map(item => item.name).join(', ')} + )} + + + {isPopoverOpen && ( +
+
    + {selectedUsers.map((item, index) => { + return ( +
  • + + {item.name} + +
  • + ); + })} +
+
+ +
    + {filteredAvailableUsers.map((item, index) => { + return ( +
  • +
    + + {item.name} +
    + {item.isSelected && } +
  • + ); + })} +
+
+
+ )} +
+ ); + } +} + +UserSelector.propTypes = propTypes; + +export default UserSelector; diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css index 8a0d8a725d..8ecf9078a4 100644 --- a/media/css/seahub_react.css +++ b/media/css/seahub_react.css @@ -525,7 +525,7 @@ a, a:hover { color: #ec8000; } } .nav .nav-item .nav-link { padding: 0.5rem 0; - margin-right: 0.5rem; + margin-right: 30px; color: #8A948F; font-weight: normal; transition: none; diff --git a/seahub/urls.py b/seahub/urls.py index db683eaeb9..47ce9ed54f 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -275,6 +275,7 @@ urlpatterns = [ ### React ### path('dashboard/', react_fake_view, name="dashboard"), + path('my-activities/', react_fake_view, name="my_activities"), path('starred/', react_fake_view, name="starred"), path('linked-devices/', react_fake_view, name="linked_devices"), path('share-admin-libs/', react_fake_view, name="share_admin_libs"),