mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-15 21:53:37 +00:00
['Activities'] redesigned it: offer 2 nav tabs 'All Activities' & 'My Activities'; offer 'Modified by' user filter (#5747)
This commit is contained in:
parent
6d9ecc6fa9
commit
d12a619e5f
@ -11,6 +11,7 @@ import MainPanel from './components/main-panel';
|
|||||||
import DraftsView from './pages/drafts/drafts-view';
|
import DraftsView from './pages/drafts/drafts-view';
|
||||||
import DraftContent from './pages/drafts/draft-content';
|
import DraftContent from './pages/drafts/draft-content';
|
||||||
import FilesActivities from './pages/dashboard/files-activities';
|
import FilesActivities from './pages/dashboard/files-activities';
|
||||||
|
import MyFileActivities from './pages/dashboard/my-file-activities';
|
||||||
import Starred from './pages/starred/starred';
|
import Starred from './pages/starred/starred';
|
||||||
import LinkedDevices from './pages/linked-devices/linked-devices';
|
import LinkedDevices from './pages/linked-devices/linked-devices';
|
||||||
import editUtilities from './utils/editor-utilities';
|
import editUtilities from './utils/editor-utilities';
|
||||||
@ -37,6 +38,7 @@ import './css/toolbar.css';
|
|||||||
import './css/search.css';
|
import './css/search.css';
|
||||||
|
|
||||||
const FilesActivitiesWrapper = MainContentWrapper(FilesActivities);
|
const FilesActivitiesWrapper = MainContentWrapper(FilesActivities);
|
||||||
|
const MyFileActivitiesWrapper = MainContentWrapper(MyFileActivities);
|
||||||
const DraftsViewWrapper = MainContentWrapper(DraftsView);
|
const DraftsViewWrapper = MainContentWrapper(DraftsView);
|
||||||
const StarredWrapper = MainContentWrapper(Starred);
|
const StarredWrapper = MainContentWrapper(Starred);
|
||||||
const LinkedDevicesWrapper = MainContentWrapper(LinkedDevices);
|
const LinkedDevicesWrapper = MainContentWrapper(LinkedDevices);
|
||||||
@ -241,6 +243,7 @@ class App extends Component {
|
|||||||
<Router className="reach-router">
|
<Router className="reach-router">
|
||||||
{home}
|
{home}
|
||||||
<FilesActivitiesWrapper path={siteRoot + 'dashboard'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
|
<FilesActivitiesWrapper path={siteRoot + 'dashboard'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
|
||||||
|
<MyFileActivitiesWrapper path={siteRoot + 'my-activities'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
|
||||||
<DraftsViewWrapper path={siteRoot + 'drafts'}
|
<DraftsViewWrapper path={siteRoot + 'drafts'}
|
||||||
onShowSidePanel={this.onShowSidePanel}
|
onShowSidePanel={this.onShowSidePanel}
|
||||||
onSearchedClick={this.onSearchedClick}
|
onSearchedClick={this.onSearchedClick}
|
||||||
|
@ -9,3 +9,56 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: .2em;
|
margin-bottom: .2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.activity-modifier:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-modifier .toggle-icon {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-modifier-selector-container {
|
||||||
|
width: 320px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
margin-top: 2px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-selected-modifiers {
|
||||||
|
min-height: 2rem;
|
||||||
|
background: #f6f6f6;
|
||||||
|
border-bottom: 1px solid #dde2ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-selected-modifier {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
padding: 0 8px 0 2px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #eaeaea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unselect-activity-user {
|
||||||
|
color: #909090;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unselect-activity-user:hover {
|
||||||
|
color: #5a5a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-user-list {
|
||||||
|
min-height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-user-item {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-user-item:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
241
frontend/src/pages/dashboard/activity-item.js
Normal file
241
frontend/src/pages/dashboard/activity-item.js
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { gettext, siteRoot, serviceURL } from '../../utils/constants';
|
||||||
|
import { Utils } from '../../utils/utils';
|
||||||
|
import ListCreatedFileDialog from '../../components/dialog/list-created-files-dialog';
|
||||||
|
import ModalPortal from '../../components/modal-portal';
|
||||||
|
|
||||||
|
import '../../css/files-activities.css';
|
||||||
|
|
||||||
|
moment.locale(window.app.config.lang);
|
||||||
|
|
||||||
|
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 = <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 = libLink;
|
||||||
|
break;
|
||||||
|
case 'rename':
|
||||||
|
op = gettext('Renamed library');
|
||||||
|
details = <span>{item.old_repo_name} => {libLink}</span>;
|
||||||
|
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 = <a href={fileURL} target="_blank" rel="noreferrer">{item.name}</a>;
|
||||||
|
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 = `<a href=${fileURL} target="_blank">${item.name}</a>`;
|
||||||
|
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 = (
|
||||||
|
<Fragment>
|
||||||
|
<p className="m-0 d-inline" dangerouslySetInnerHTML={{__html: firstLine}}></p>
|
||||||
|
{isDesktop && <button type="button" onClick={this.onListCreatedFilesToggle} className="activity-details text-secondary ml-2 border-0 p-0 bg-transparent">{gettext('details')}</button>}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
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 = <a href={fileURL} target="_blank" rel="noreferrer">{item.name}</a>;
|
||||||
|
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 = <span>{item.old_name} => {fileLink}</span>;
|
||||||
|
moreDetails = true;
|
||||||
|
break;
|
||||||
|
case 'move':
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const filePathLink = <a href={fileURL}>{item.path}</a>;
|
||||||
|
op = gettext('Moved file');
|
||||||
|
details = <span>{item.old_path} => {filePathLink}</span>;
|
||||||
|
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 = <a href={dirURL} target="_blank" rel="noreferrer">{item.name}</a>;
|
||||||
|
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 = <span>{item.old_name} => {dirLink}</span>;
|
||||||
|
moreDetails = true;
|
||||||
|
break;
|
||||||
|
case 'move':
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const dirPathLink = <a href={dirURL}>{item.path}</a>;
|
||||||
|
op = gettext('Moved folder');
|
||||||
|
details = <span>{item.old_path} => {dirPathLink}</span>;
|
||||||
|
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 (
|
||||||
|
<Fragment>
|
||||||
|
{isShowDate &&
|
||||||
|
<tr>
|
||||||
|
<td colSpan={isDesktop ? 5 : 3} className="border-top-0">{moment(item.time).format('YYYY-MM-DD')}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
{isDesktop ? (
|
||||||
|
<tr>
|
||||||
|
<td className="text-center">
|
||||||
|
<img src={item.avatar_url} alt="" width="32" height="32" className="avatar" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href={userProfileURL}>{item.author_name}</a>
|
||||||
|
</td>
|
||||||
|
<td>{op}</td>
|
||||||
|
<td>
|
||||||
|
{details}
|
||||||
|
{moreDetails && <br /> }
|
||||||
|
{moreDetails && smallLibLink}
|
||||||
|
</td>
|
||||||
|
<td className="text-secondary">
|
||||||
|
<time datetime={item.time} is="relative-time" title={moment(item.time).format('llll')}>{moment(item.time).fromNow()}</time>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td className="text-center align-top">
|
||||||
|
<img src={item.avatar_url} alt="" width="32" height="32" className="avatar" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href={userProfileURL}>{item.author_name}</a>
|
||||||
|
<p className="m-0 text-secondary">{op}</p>
|
||||||
|
{details}
|
||||||
|
</td>
|
||||||
|
<td className="text-right align-top">
|
||||||
|
<span className="text-secondary mobile-activity-time">
|
||||||
|
<time datetime={item.time} is="relative-time" title={moment(item.time).format('llll')}>{moment(item.time).fromNow()}</time>
|
||||||
|
</span>
|
||||||
|
{moreDetails && <br /> }
|
||||||
|
{moreDetails && libLink}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{this.state.isListCreatedFiles &&
|
||||||
|
<ModalPortal>
|
||||||
|
<ListCreatedFileDialog
|
||||||
|
activity={item}
|
||||||
|
toggleCancel={this.onListCreatedFilesToggle}
|
||||||
|
/>
|
||||||
|
</ModalPortal>
|
||||||
|
}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ActivityItem.propTypes = activityPropTypes;
|
||||||
|
|
||||||
|
export default ActivityItem;
|
76
frontend/src/pages/dashboard/content.js
Normal file
76
frontend/src/pages/dashboard/content.js
Normal file
@ -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 <EmptyTip><p>{gettext('No more activities')}</p></EmptyTip>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const desktopThead = (
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="8%">{/* avatar */}</th>
|
||||||
|
<th width="15%">{gettext('User')}</th>
|
||||||
|
<th width="20%">{gettext('Operation')}</th>
|
||||||
|
<th width="37%">{gettext('File')} / {gettext('Library')}</th>
|
||||||
|
<th width="20%">{gettext('Time')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
|
|
||||||
|
const mobileThead = (
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="15%"></th>
|
||||||
|
<th width="53%"></th>
|
||||||
|
<th width="32%"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<table className="table-hover table-thead-hidden">
|
||||||
|
{isDesktop ? desktopThead : mobileThead}
|
||||||
|
<tbody>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<ActivityItem
|
||||||
|
key={index}
|
||||||
|
isDesktop={isDesktop}
|
||||||
|
item={item}
|
||||||
|
index={index}
|
||||||
|
items={items}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{isLoadingMore ? <span className="loading-icon loading-tip"></span> : ''}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileActivitiesContent.propTypes = contentPropTypes;
|
||||||
|
|
||||||
|
export default FileActivitiesContent;
|
@ -1,306 +1,25 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { Link } from '@gatsbyjs/reach-router';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
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 { Utils } from '../../utils/utils';
|
||||||
import Loading from '../../components/loading';
|
import Loading from '../../components/loading';
|
||||||
import Activity from '../../models/activity';
|
import Activity from '../../models/activity';
|
||||||
import ListCreatedFileDialog from '../../components/dialog/list-created-files-dialog';
|
import FileActivitiesContent from './content';
|
||||||
import ModalPortal from '../../components/modal-portal';
|
import UserSelector from './user-selector';
|
||||||
|
|
||||||
import '../../css/files-activities.css';
|
import '../../css/files-activities.css';
|
||||||
|
|
||||||
moment.locale(window.app.config.lang);
|
moment.locale(window.app.config.lang);
|
||||||
|
|
||||||
const contentPropTypes = {
|
const propTypes = {
|
||||||
isLoadingMore: PropTypes.bool.isRequired,
|
onlyMine: PropTypes.bool
|
||||||
items: PropTypes.array.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class FileActivitiesContent extends Component {
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const isDesktop = Utils.isDesktop();
|
|
||||||
let { items, isLoadingMore } = this.props;
|
|
||||||
|
|
||||||
const desktopThead = (
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th width="8%">{/* avatar */}</th>
|
|
||||||
<th width="15%">{gettext('User')}</th>
|
|
||||||
<th width="20%">{gettext('Operation')}</th>
|
|
||||||
<th width="37%">{gettext('File')} / {gettext('Library')}</th>
|
|
||||||
<th width="20%">{gettext('Time')}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
);
|
|
||||||
|
|
||||||
const mobileThead = (
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th width="15%"></th>
|
|
||||||
<th width="53%"></th>
|
|
||||||
<th width="32%"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<table className="table-hover table-thead-hidden">
|
|
||||||
{isDesktop ? desktopThead : mobileThead}
|
|
||||||
<tbody>
|
|
||||||
{items.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<ActivityItem
|
|
||||||
key={index}
|
|
||||||
isDesktop={isDesktop}
|
|
||||||
item={item}
|
|
||||||
index={index}
|
|
||||||
items={items}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{isLoadingMore ? <span className="loading-icon loading-tip"></span> : ''}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = <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 = libLink;
|
|
||||||
break;
|
|
||||||
case 'rename':
|
|
||||||
op = gettext('Renamed library');
|
|
||||||
details = <span>{item.old_repo_name} => {libLink}</span>;
|
|
||||||
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 = <a href={fileURL} target="_blank" rel="noreferrer">{item.name}</a>;
|
|
||||||
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 = `<a href=${fileURL} target="_blank">${item.name}</a>`;
|
|
||||||
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 = (
|
|
||||||
<Fragment>
|
|
||||||
<p className="m-0 d-inline" dangerouslySetInnerHTML={{__html: firstLine}}></p>
|
|
||||||
{isDesktop && <button type="button" onClick={this.onListCreatedFilesToggle} className="activity-details text-secondary ml-2 border-0 p-0 bg-transparent">{gettext('details')}</button>}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
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 = <a href={fileURL} target="_blank" rel="noreferrer">{item.name}</a>;
|
|
||||||
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 = <span>{item.old_name} => {fileLink}</span>;
|
|
||||||
moreDetails = true;
|
|
||||||
break;
|
|
||||||
case 'move':
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const filePathLink = <a href={fileURL}>{item.path}</a>;
|
|
||||||
op = gettext('Moved file');
|
|
||||||
details = <span>{item.old_path} => {filePathLink}</span>;
|
|
||||||
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 = <a href={dirURL} target="_blank" rel="noreferrer">{item.name}</a>;
|
|
||||||
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 = <span>{item.old_name} => {dirLink}</span>;
|
|
||||||
moreDetails = true;
|
|
||||||
break;
|
|
||||||
case 'move':
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const dirPathLink = <a href={dirURL}>{item.path}</a>;
|
|
||||||
op = gettext('Moved folder');
|
|
||||||
details = <span>{item.old_path} => {dirPathLink}</span>;
|
|
||||||
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 (
|
|
||||||
<Fragment>
|
|
||||||
{isShowDate &&
|
|
||||||
<tr>
|
|
||||||
<td colSpan={isDesktop ? 5 : 3} className="border-top-0">{moment(item.time).format('YYYY-MM-DD')}</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
{isDesktop ? (
|
|
||||||
<tr>
|
|
||||||
<td className="text-center">
|
|
||||||
<img src={item.avatar_url} alt="" width="32" height="32" className="avatar" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href={userProfileURL}>{item.author_name}</a>
|
|
||||||
</td>
|
|
||||||
<td>{op}</td>
|
|
||||||
<td>
|
|
||||||
{details}
|
|
||||||
{moreDetails && <br /> }
|
|
||||||
{moreDetails && smallLibLink}
|
|
||||||
</td>
|
|
||||||
<td className="text-secondary">
|
|
||||||
<time datetime={item.time} is="relative-time" title={moment(item.time).format('llll')}>{moment(item.time).fromNow()}</time>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : (
|
|
||||||
<tr>
|
|
||||||
<td className="text-center align-top">
|
|
||||||
<img src={item.avatar_url} alt="" width="32" height="32" className="avatar" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href={userProfileURL}>{item.author_name}</a>
|
|
||||||
<p className="m-0 text-secondary">{op}</p>
|
|
||||||
{details}
|
|
||||||
</td>
|
|
||||||
<td className="text-right align-top">
|
|
||||||
<span className="text-secondary mobile-activity-time">
|
|
||||||
<time datetime={item.time} is="relative-time" title={moment(item.time).format('llll')}>{moment(item.time).fromNow()}</time>
|
|
||||||
</span>
|
|
||||||
{moreDetails && <br /> }
|
|
||||||
{moreDetails && libLink}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
{this.state.isListCreatedFiles &&
|
|
||||||
<ModalPortal>
|
|
||||||
<ListCreatedFileDialog
|
|
||||||
activity={item}
|
|
||||||
toggleCancel={this.onListCreatedFilesToggle}
|
|
||||||
/>
|
|
||||||
</ModalPortal>
|
|
||||||
}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ActivityItem.propTypes = activityPropTypes;
|
|
||||||
|
|
||||||
class FilesActivities extends Component {
|
class FilesActivities extends Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -309,21 +28,37 @@ class FilesActivities extends Component {
|
|||||||
isLoadingMore: false,
|
isLoadingMore: false,
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
|
allItems: [],
|
||||||
items: [],
|
items: [],
|
||||||
|
availableUsers: [],
|
||||||
|
targetUsers: []
|
||||||
};
|
};
|
||||||
this.avatarSize = 72;
|
this.avatarSize = 72;
|
||||||
this.curPathList = [];
|
this.curPathList = [];
|
||||||
this.oldPathList = [];
|
this.oldPathList = [];
|
||||||
|
this.availableUserEmails = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
let currentPage = this.state.currentPage;
|
let { currentPage, availableUsers } = this.state;
|
||||||
seafileAPI.listActivities(currentPage, this.avatarSize).then(res => {
|
seafileAPI.listActivities(currentPage, this.avatarSize).then(res => {
|
||||||
// {"events":[...]}
|
// {"events":[...]}
|
||||||
let events = this.mergePublishEvents(res.data.events);
|
let events = this.mergePublishEvents(res.data.events);
|
||||||
events = this.mergeFileCreateEvents(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({
|
this.setState({
|
||||||
items: events,
|
allItems: events,
|
||||||
|
items: this.filterEvents(events),
|
||||||
|
availableUsers: availableUsers,
|
||||||
currentPage: currentPage + 1,
|
currentPage: currentPage + 1,
|
||||||
isFirstLoading: false,
|
isFirstLoading: false,
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
@ -400,19 +135,34 @@ class FilesActivities extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getMore() {
|
getMore() {
|
||||||
let currentPage = this.state.currentPage;
|
const { currentPage, availableUsers, targetUsers } = this.state;
|
||||||
seafileAPI.listActivities(currentPage, this.avatarSize).then(res => {
|
seafileAPI.listActivities(currentPage, this.avatarSize).then(res => {
|
||||||
// {"events":[...]}
|
// {"events":[...]}
|
||||||
let events = this.mergePublishEvents(res.data.events);
|
let events = this.mergePublishEvents(res.data.events);
|
||||||
events = this.mergeFileCreateEvents(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({
|
this.setState({
|
||||||
isLoadingMore: false,
|
allItems: [...this.state.allItems, ...events],
|
||||||
items: [...this.state.items, ...events],
|
items: [...this.state.items, ...filteredEvents],
|
||||||
|
availableUsers: availableUsers,
|
||||||
currentPage: currentPage + 1,
|
currentPage: currentPage + 1,
|
||||||
|
isLoadingMore: false,
|
||||||
hasMore: res.data.events.length === 0 ? false : true
|
hasMore: res.data.events.length === 0 ? false : true
|
||||||
});
|
});
|
||||||
if (this.state.items.length < 25 && this.state.hasMore) {
|
if (this.state.items.length < 25 && this.state.hasMore) {
|
||||||
this.getMore();
|
if (!(targetUsers.length && currentPage == 100)) {
|
||||||
|
this.getMore();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
this.setState({
|
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) => {
|
handleScroll = (event) => {
|
||||||
if (!this.state.isLoadingMore && this.state.hasMore) {
|
if (!this.state.isLoadingMore && this.state.hasMore) {
|
||||||
const clientHeight = event.target.clientHeight;
|
const clientHeight = event.target.clientHeight;
|
||||||
@ -437,19 +215,38 @@ class FilesActivities extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { onlyMine } = this.props;
|
||||||
|
const { targetUsers, availableUsers } = this.state;
|
||||||
return (
|
return (
|
||||||
<div className="main-panel-center">
|
<div className="main-panel-center">
|
||||||
<div className="cur-view-container" id="activities">
|
<div className="cur-view-container" id="activities">
|
||||||
<div className="cur-view-path">
|
<div className="cur-view-path">
|
||||||
<h3 className="sf-heading">{gettext('Activities')}</h3>
|
<ul className="nav">
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link to={`${siteRoot}dashboard/`} className={`nav-link${onlyMine ? '' : ' active'}`}>{gettext('All Activities')}</Link>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link to={`${siteRoot}my-activities/`} className={`nav-link${onlyMine ? ' active': ''}`}>{gettext('My Activities')}</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="cur-view-content d-block" onScroll={this.handleScroll}>
|
<div className="cur-view-content d-block" onScroll={this.handleScroll}>
|
||||||
{this.state.isFirstLoading && <Loading />}
|
{this.state.isFirstLoading && <Loading />}
|
||||||
{(!this.state.isFirstLoading && this.state.errorMsg) &&
|
{(!this.state.isFirstLoading && this.state.errorMsg) &&
|
||||||
<p className="error text-center">{this.state.errorMsg}</p>
|
<p className="error text-center">{this.state.errorMsg}</p>
|
||||||
}
|
}
|
||||||
{!this.state.isFirstLoading &&
|
{!this.state.isFirstLoading && (
|
||||||
<FileActivitiesContent items={this.state.items} isLoadingMore={this.state.isLoadingMore}/>
|
<Fragment>
|
||||||
|
{!onlyMine && (
|
||||||
|
<UserSelector
|
||||||
|
availableUsers={availableUsers}
|
||||||
|
currentSelectedUsers={targetUsers}
|
||||||
|
setTargetUsers={this.setTargetUsers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<FileActivitiesContent items={this.state.items} isLoadingMore={this.state.isLoadingMore} />
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -458,4 +255,6 @@ class FilesActivities extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FilesActivities.propTypes = propTypes;
|
||||||
|
|
||||||
export default FilesActivities;
|
export default FilesActivities;
|
||||||
|
11
frontend/src/pages/dashboard/my-file-activities.js
Normal file
11
frontend/src/pages/dashboard/my-file-activities.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import FilesActivities from './files-activities';
|
||||||
|
|
||||||
|
class MyFilesActivities extends Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <FilesActivities onlyMine={true} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyFilesActivities;
|
122
frontend/src/pages/dashboard/user-selector.js
Normal file
122
frontend/src/pages/dashboard/user-selector.js
Normal file
@ -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 (
|
||||||
|
<div className="mt-4 position-relative">
|
||||||
|
<span className="d-inline-block p-2 activity-modifier rounded" onClick={this.togglePopover}>
|
||||||
|
{gettext('Modified by:')}
|
||||||
|
{currentSelectedUsers.length > 0 && (
|
||||||
|
<span className="d-inline-block ml-1">{currentSelectedUsers.map(item => item.name).join(', ')}</span>
|
||||||
|
)}
|
||||||
|
<i className="fas fa-caret-down ml-2 toggle-icon"></i>
|
||||||
|
</span>
|
||||||
|
{isPopoverOpen && (
|
||||||
|
<div className="position-absolute activity-modifier-selector-container rounded shadow">
|
||||||
|
<ul className="activity-selected-modifiers px-3 py-2 list-unstyled d-flex">
|
||||||
|
{selectedUsers.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<li key={index} className="activity-selected-modifier">
|
||||||
|
<img src={item.avatar_url} className="select-module select-module-icon avatar" alt="" />
|
||||||
|
<span className='select-module select-module-name'>{item.name}</span>
|
||||||
|
<i className="sf2-icon-close unselect-activity-user ml-2" onClick={this.toggleSelectItem.bind(this, item)}></i>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<div className="p-3">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={gettext('Search users...')}
|
||||||
|
className="mb-1"
|
||||||
|
onKeyDown={this.searchUsers}
|
||||||
|
/>
|
||||||
|
<ul className="activity-user-list list-unstyled">
|
||||||
|
{filteredAvailableUsers.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<li key={index} className="activity-user-item h-6 p-1 rounded d-flex justify-content-between align-items-center" onClick={this.toggleSelectItem.bind(this, item)}>
|
||||||
|
<div>
|
||||||
|
<img src={item.avatar_url} className="select-module select-module-icon avatar" alt="" />
|
||||||
|
<span className='select-module select-module-name'>{item.name}</span>
|
||||||
|
</div>
|
||||||
|
{item.isSelected && <i className="sf2-icon-tick text-gray"></i>}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UserSelector.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default UserSelector;
|
@ -525,7 +525,7 @@ a, a:hover { color: #ec8000; }
|
|||||||
}
|
}
|
||||||
.nav .nav-item .nav-link {
|
.nav .nav-item .nav-link {
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
margin-right: 0.5rem;
|
margin-right: 30px;
|
||||||
color: #8A948F;
|
color: #8A948F;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
transition: none;
|
transition: none;
|
||||||
|
@ -275,6 +275,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
### React ###
|
### React ###
|
||||||
path('dashboard/', react_fake_view, name="dashboard"),
|
path('dashboard/', react_fake_view, name="dashboard"),
|
||||||
|
path('my-activities/', react_fake_view, name="my_activities"),
|
||||||
path('starred/', react_fake_view, name="starred"),
|
path('starred/', react_fake_view, name="starred"),
|
||||||
path('linked-devices/', react_fake_view, name="linked_devices"),
|
path('linked-devices/', react_fake_view, name="linked_devices"),
|
||||||
path('share-admin-libs/', react_fake_view, name="share_admin_libs"),
|
path('share-admin-libs/', react_fake_view, name="share_admin_libs"),
|
||||||
|
Loading…
Reference in New Issue
Block a user