1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-03 07:55:36 +00:00

Show draft review info at repo info bar (#2832)

This commit is contained in:
C_Q
2019-01-18 14:50:42 +08:00
committed by Daniel Pan
parent f0e9c7a1b1
commit d1f55247ff
14 changed files with 450 additions and 30 deletions

View File

@@ -0,0 +1,80 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import { gettext, siteRoot } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import moment from 'moment';
import { Utils } from '../../utils/utils';
import Draft from '../../models/draft';
const propTypes = {
repoID: PropTypes.string.isRequired,
toggle: PropTypes.func.isRequired,
};
class ListRepoDraftsDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
drafts: [],
};
}
componentDidMount() {
seafileAPI.listRepoDrafts(this.props.repoID).then(res => {
let drafts = res.data.drafts.map(item => {
let draft = new Draft(item);
return draft;
})
this.setState({
drafts: drafts
});
})
}
toggle = () => {
this.props.toggle();
}
render() {
let drafts = this.state.drafts;
return (
<Modal isOpen={true}>
<ModalHeader toggle={this.toggle}>{gettext('Drafts')}</ModalHeader>
<ModalBody className="dialog-list-container">
<table>
<thead>
<tr>
<th width='50%' className="ellipsis">{gettext('Name')}</th>
<th width='25%'>{gettext('Owner')}</th>
<th width='25%'>{gettext('Last Update')}</th>
</tr>
</thead>
<tbody>
{this.state.drafts.map((draft) => {
let href = siteRoot + 'lib/' + draft.originRepoID + '/file' + Utils.encodePath(draft.draftFilePath);
return (
<tr key={draft.id}>
<td className="name">
<a href={href} target='_blank'>{Utils.getFileName(draft.draftFilePath)}</a>
</td>
<td>{draft.ownerNickname}</td>
<td>{moment(draft.createdStr).fromNow()}</td>
</tr>
);
})}
</tbody>
</table>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Close')}</Button>
</ModalFooter>
</Modal>
);
}
}
ListRepoDraftsDialog.propTypes = propTypes;
export default ListRepoDraftsDialog;

View File

@@ -0,0 +1,81 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import { gettext, siteRoot } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import moment from 'moment';
import { Utils } from '../../utils/utils';
import Review from '../../models/review';
const propTypes = {
repoID: PropTypes.string.isRequired,
toggle: PropTypes.func.isRequired,
};
class ListRepoReviewsDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
reviews: [],
};
}
componentDidMount() {
seafileAPI.listRepoReviews(this.props.repoID).then(res => {
let reviews = res.data.reviews.map(item =>{
let review = new Review(item);
return review;
})
this.setState({
reviews: reviews
});
});
}
toggle = () => {
this.props.toggle();
}
render() {
let reviews = this.state.reviews;
return (
<Modal isOpen={true}>
<ModalHeader toggle={this.toggle}>{gettext('Reviews')}</ModalHeader>
<ModalBody className="dialog-list-container">
<table>
<thead>
<tr>
<th width='50%' className="ellipsis">{gettext('Name')}</th>
<th width='25%'>{gettext('Owner')}</th>
<th width='25%'>{gettext('Last Update')}</th>
</tr>
</thead>
<tbody>
{this.state.reviews.map((review) => {
let href = siteRoot + 'drafts/review/' + review.id;
return (
<tr key={review.id}>
<td className="name">
<a href={href} target='_blank'>{Utils.getFileName(review.draftFilePath)}</a>
</td>
<td>{review.creatorName}</td>
<td>{moment(review.createdStr).fromNow()}</td>
</tr>
);
})}
</tbody>
</table>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Close')}</Button>
</ModalFooter>
</Modal>
);
}
}
ListRepoReviewsDialog.propTypes = propTypes;
export default ListRepoReviewsDialog;

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import { gettext, siteRoot } from '../../utils/constants';
@@ -44,13 +44,10 @@ class ListTaggedFilesDialog extends React.Component {
render() {
let taggedFileList = this.state.taggedFileList;
return (
<Fragment>
<ModalHeader toggle={this.props.onClose}>
<span className="tag-dialog-back fas fa-sm fa-arrow-left" onClick={this.props.toggleCancel} aria-label={gettext('Back')}></span>
{gettext('Tagged Files')}
</ModalHeader>
<Modal isOpen={true}>
<ModalHeader toggle={this.props.onClose}>{gettext('Tagged Files')}</ModalHeader>
<ModalBody className="dialog-list-container">
<table className="table-thead-hidden">
<table>
<thead>
<tr>
<th width='50%' className="ellipsis">{gettext('Name')}</th>
@@ -78,7 +75,7 @@ class ListTaggedFilesDialog extends React.Component {
<ModalFooter>
<Button color="secondary" onClick={this.props.toggleCancel}>{gettext('Close')}</Button>
</ModalFooter>
</Fragment>
</Modal>
);
}
}

View File

@@ -52,6 +52,8 @@ const propTypes = {
onFileUploadSuccess: PropTypes.func.isRequired,
usedRepoTags: PropTypes.array.isRequired,
readmeMarkdown: PropTypes.object,
draftCounts: PropTypes.number,
reviewCounts: PropTypes.number,
};
class DirPanel extends React.Component {
@@ -129,7 +131,9 @@ class DirPanel extends React.Component {
render() {
const errMessage = (<div className="message empty-tip err-message"><h2>{gettext('Folder does not exist.')}</h2></div>);
const showRepoInfoBar = this.props.path === '/' && (
this.props.usedRepoTags.length != 0 || this.props.readmeMarkdown != null ||
this.props.draftCounts != 0 || this.props.reviewCounts != 0);
return (
<div className="main-panel wiki-main-panel o-hidden">
<div className="main-panel-north">
@@ -191,12 +195,14 @@ class DirPanel extends React.Component {
{!this.props.pathExist ?
errMessage :
<Fragment>
{this.props.path === '/' && !(this.props.usedRepoTags.length === 0 && this.props.readmeMarkdown === null) && (
{showRepoInfoBar && (
<RepoInfoBar
repoID={this.props.repoID}
currentPath={this.props.path}
usedRepoTags={this.props.usedRepoTags}
readmeMarkdown={this.props.readmeMarkdown}
draftCounts={this.props.draftCounts}
reviewCounts={this.props.reviewCounts}
/>
)}
<DirentListView

View File

@@ -44,6 +44,8 @@ class DirView extends React.Component {
errorMsg: '',
usedRepoTags: [],
readmeMarkdown: null,
draftCounts: 0,
reviewCounts: 0,
};
window.onpopstate = this.onpopstate;
this.lastModifyTime = new Date();
@@ -61,6 +63,12 @@ class DirView extends React.Component {
let location = decodeURIComponent(window.location.href);
let repoID = this.props.repoID;
collabServer.watchRepo(repoID, this.onRepoUpdateEvent);
seafileAPI.getRepoDraftReviewCounts(repoID).then(res => {
this.setState({
draftCounts: res.data.draft_counts,
reviewCounts: res.data.review_counts,
})
})
seafileAPI.listRepoTags(repoID).then(res => {
let usedRepoTags = [];
res.data.repo_tags.forEach(item => {
@@ -696,6 +704,8 @@ class DirView extends React.Component {
onLibDecryptDialog={this.onLibDecryptDialog}
usedRepoTags={this.state.usedRepoTags}
readmeMarkdown={this.state.readmeMarkdown}
draftCounts={this.state.draftCounts}
reviewCounts={this.state.reviewCounts}
/>
);
}

View File

@@ -3,7 +3,9 @@ import PropTypes from 'prop-types';
import ModalPortal from './modal-portal';
import { Modal } from 'reactstrap';
import ListTaggedFilesDialog from './dialog/list-taggedfiles-dialog';
import { siteRoot } from '../utils/constants';
import ListRepoDraftsDialog from './dialog/list-repo-drafts-dialog';
import ListRepoReviewsDialog from './dialog/list-repo-reviews-dialog';
import { siteRoot, gettext } from '../utils/constants';
import { Utils } from '../utils/utils';
import '../css/repo-info-bar.css';
@@ -13,6 +15,8 @@ const propTypes = {
currentPath: PropTypes.string.isRequired,
usedRepoTags: PropTypes.array.isRequired,
readmeMarkdown: PropTypes.object,
draftCounts: PropTypes.number,
reviewCounts: PropTypes.number,
};
class RepoInfoBar extends React.Component {
@@ -22,6 +26,8 @@ class RepoInfoBar extends React.Component {
this.state = {
currentTag: null,
isListTaggedFileShow: false,
showRepoDrafts: false,
showRepoReviews: false,
};
}
@@ -38,6 +44,18 @@ class RepoInfoBar extends React.Component {
});
}
toggleDrafts = () => {
this.setState({
showRepoDrafts: !this.state.showRepoDrafts
});
}
toggleReviews = () => {
this.setState({
showRepoReviews: !this.state.showRepoReviews
});
}
render() {
let {repoID, currentPath, usedRepoTags, readmeMarkdown} = this.props;
let href = readmeMarkdown !== null ? siteRoot + 'lib/' + repoID + '/file' + Utils.joinPath(currentPath, readmeMarkdown.name) : '';
@@ -59,24 +77,61 @@ class RepoInfoBar extends React.Component {
})}
</ul>
)}
<div className="readme-files">
{readmeMarkdown !== null && (
<div className="readme-file">
<span className="readme-file">
<i className="readme-flag fa fa-flag"></i>
<a className="readme-name" href={href} target='_blank'>{readmeMarkdown.name}</a>
</div>
</span>
)}
{this.props.draftCounts > 0 &&
<span className="readme-file">
<i className="readme-flag fa fa-pen"></i>
<span className="used-tag-name">{gettext('draft')}</span>
<span className="used-tag-files" onClick={this.toggleDrafts}>
{this.props.draftCounts > 1 ? this.props.draftCounts + ' files' : this.props.draftCounts + ' file'}
</span>
</span>
}
{this.props.reviewCounts > 0 &&
<span className="readme-file">
<i className="readme-flag fa fa-clipboard"></i>
<span className="used-tag-name">{gettext('review')}</span>
<span className="used-tag-files" onClick={this.toggleReviews}>
{this.props.reviewCounts > 1 ? this.props.reviewCounts + ' files' : this.props.reviewCounts + ' file'}
</span>
</span>
}
</div>
{this.state.isListTaggedFileShow && (
<ModalPortal>
<Modal isOpen={true}>
<ListTaggedFilesDialog
repoID={repoID}
currentTag={this.state.currentTag}
onClose={this.onCloseDialog}
toggleCancel={this.onListTaggedFiles}
/>
</Modal>
</ModalPortal>
)}
{this.state.showRepoDrafts && (
<ModalPortal>
<ListRepoDraftsDialog
toggle={this.toggleDrafts}
repoID={this.props.repoID}
/>
</ModalPortal>
)}
{this.state.showRepoReviews && (
<ModalPortal>
<ListRepoReviewsDialog
toggle={this.toggleReviews}
repoID={this.props.repoID}
/>
</ModalPortal>
)}
</div>
);
}

View File

@@ -1,6 +1,5 @@
.repo-info-bar {
margin-bottom: 5px;
padding: 0 10px;
padding: 10px;
border: 1px solid #e6e6dd;
border-radius: 5px;
background: #f8f8f8;
@@ -8,7 +7,6 @@
.used-tag-list {
list-style: none;
margin: 8px 0;
}
.used-tag-item {
@@ -38,7 +36,8 @@
}
.readme-file {
margin: 8px 15px;
margin: 0 15px;
display: inline-block;
}
.readme-file a {

View File

@@ -0,0 +1,15 @@
import moment from 'moment';
class Draft {
constructor(item) {
this.created = item.created_at;
this.createdStr = moment((new Date(item.created_at)).getTime()).format('YYYY-MM-DD HH:mm');
this.id = item.id;
this.ownerNickname = item.owner_nickname;
this.originRepoID = item.origin_repo_id;
this.draftFilePath = item.draft_file_path;
}
}
export default Draft;

View File

@@ -0,0 +1,14 @@
import moment from 'moment';
class Review {
constructor(item) {
this.created = item.created_at;
this.createdStr = moment((new Date(item.created_at)).getTime()).format('YYYY-MM-DD HH:mm');
this.id = item.id;
this.creatorName = item.creator_name;
this.draftFilePath = item.draft_file_path;
}
}
export default Review;

View File

@@ -63,6 +63,8 @@ const propTypes = {
reviewID: PropTypes.any,
usedRepoTags: PropTypes.array.isRequired,
readmeMarkdown: PropTypes.object,
draftCounts: PropTypes.number,
reviewCounts: PropTypes.number,
};
class MainPanel extends Component {
@@ -160,6 +162,9 @@ class MainPanel extends Component {
render() {
const ErrMessage = (<div className="message empty-tip err-message"><h2>{gettext('Folder does not exist.')}</h2></div>);
const showRepoInfoBar = this.props.path === '/' && (
this.props.usedRepoTags.length != 0 || this.props.readmeMarkdown != null ||
this.props.draftCounts != 0 || this.props.reviewCounts != 0);
return (
<div className="main-panel wiki-main-panel o-hidden">
@@ -240,12 +245,14 @@ class MainPanel extends Component {
</Fragment>
</WikiMarkdownViewer> :
<Fragment>
{this.props.path === '/' && !(this.props.usedRepoTags.length === 0 && this.props.readmeMarkdown === null) && (
{showRepoInfoBar && (
<RepoInfoBar
repoID={repoID}
currentPath={this.props.path}
usedRepoTags={this.props.usedRepoTags}
readmeMarkdown={this.props.readmeMarkdown}
draftCounts={this.props.draftCounts}
reviewCounts={this.props.reviewCounts}
/>
)}
<DirentListView

View File

@@ -56,6 +56,8 @@ class Wiki extends Component {
dirID: '',
usedRepoTags: [],
readmeMarkdown: null,
draftCounts: 0,
reviewCounts: 0,
};
window.onpopstate = this.onpopstate;
this.hash = '';
@@ -71,6 +73,12 @@ class Wiki extends Component {
componentDidMount() {
collabServer.watchRepo(repoID, this.onRepoUpdateEvent);
seafileAPI.getRepoDraftReviewCounts(repoID).then(res => {
this.setState({
draftCounts: res.data.draft_counts,
reviewCounts: res.data.review_counts
});
});
seafileAPI.listRepoTags(repoID).then(res => {
let usedRepoTags = [];
res.data.repo_tags.forEach(item => {
@@ -1007,6 +1015,8 @@ class Wiki extends Component {
goReviewPage={this.goReviewPage}
usedRepoTags={this.state.usedRepoTags}
readmeMarkdown={this.state.readmeMarkdown}
draftCounts={this.state.draftCounts}
reviewCounts={this.state.reviewCounts}
/>
</div>
);

View File

@@ -0,0 +1,98 @@
# Copyright (c) 2012-2016 Seafile Ltd.
import logging
from rest_framework import status
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from seaserv import seafile_api
from django.utils.translation import ugettext as _
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error
from seahub.views import check_folder_permission
from seahub.drafts.models import Draft, DraftReview
logger = logging.getLogger(__name__)
class RepoDraftReviewCounts(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated, )
throttle_classes = (UserRateThrottle, )
def get(self, request, repo_id, format=None):
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# check perm
perm = check_folder_permission(request, repo_id, '/')
if not perm:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
result = {}
# get draft counts
result['draft_counts'] = Draft.objects.get_draft_counts_by_repo_id(repo_id)
result['review_counts'] = DraftReview.objects.get_review_counts_by_repo_id(repo_id)
return Response(result)
class RepoDraftInfo(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated, )
throttle_classes = (UserRateThrottle, )
def get(self, request, repo_id, format=None):
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# check perm
perm = check_folder_permission(request, repo_id, '/')
if not perm:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
result = {}
# list draft
drafts = Draft.objects.list_draft_by_repo_id(repo_id)
result['drafts'] = drafts
return Response(result)
class RepoReviewInfo(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated, )
throttle_classes = (UserRateThrottle, )
def get(self, request, repo_id, format=None):
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# check perm
perm = check_folder_permission(request, repo_id, '/')
if not perm:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
result = {}
# list review
reviews = DraftReview.objects.list_review_by_repo_id(repo_id)
result['reviews'] = reviews
return Response(result)

View File

@@ -28,6 +28,29 @@ class OriginalFileConflict(Exception):
class DraftManager(models.Manager):
def get_draft_counts_by_repo_id(self, repo_id):
num = self.filter(origin_repo_id=repo_id).count()
return num
def list_draft_by_repo_id(self, repo_id):
"""list draft by repo id
"""
drafts = []
qs = self.filter(origin_repo_id=repo_id)
for d in qs:
draft = {}
draft['id'] = d.id
draft['owner_nickname'] = email2nickname(d.username)
draft['origin_repo_id'] = d.origin_repo_id
draft['draft_file_path'] = d.draft_file_path
draft['created_at'] = datetime_to_isoformat_timestr(d.created_at)
drafts.append(draft)
return drafts
def list_draft_by_username(self, username, with_reviews=True):
"""list all user drafts
If with_reviews is true, return the draft associated review
@@ -246,6 +269,25 @@ class DraftReviewExist(Exception):
class DraftReviewManager(models.Manager):
def get_review_counts_by_repo_id(self, repo_id, status='open'):
num = self.filter(origin_repo_id=repo_id, status=status).count()
return num
def list_review_by_repo_id(self, repo_id, status='open'):
reviews = []
qs = self.filter(origin_repo_id=repo_id, status=status)
for review in qs:
review_obj = {}
review_obj['id'] = review.id
review_obj['creator_name'] = email2nickname(review.creator)
review_obj['created_at'] = datetime_to_isoformat_timestr(review.created_at)
review_obj['draft_file_path'] = review.draft_file_path
reviews.append(review_obj)
return reviews
def add(self, creator, draft):
try:
d_r = self.get(creator=creator, draft_id=draft)

View File

@@ -70,6 +70,8 @@ from seahub.api2.endpoints.wikis import WikisView, WikiView
from seahub.api2.endpoints.drafts import DraftsView, DraftView
from seahub.api2.endpoints.draft_reviews import DraftReviewsView, DraftReviewView
from seahub.api2.endpoints.draft_review_reviewer import DraftReviewReviewerView
from seahub.api2.endpoints.repo_draft_review_info import RepoDraftInfo, \
RepoReviewInfo, RepoDraftReviewCounts
from seahub.api2.endpoints.file_review import FileReviewView
from seahub.api2.endpoints.activities import ActivitiesView
from seahub.api2.endpoints.wiki_pages import WikiPageView, WikiPagesView, WikiPagesDirView, WikiPageContentView
@@ -373,6 +375,10 @@ urlpatterns = [
url(r'^api/v2.1/file-review/$', FileReviewView.as_view(), name='api-v2.1-file-review'),
url(r'^api/v2.1/repo/(?P<repo_id>[-0-9a-f]{36})/drafts/$', RepoDraftInfo.as_view(), name='api-v2.1-repo-drafts' ),
url(r'^api/v2.1/repo/(?P<repo_id>[-0-9a-f]{36})/reviews/$', RepoReviewInfo.as_view(), name='api-v2.1-repo-reviews' ),
url(r'^api/v2.1/repo/(?P<repo_id>[-0-9a-f]{36})/draft-review-counts/$', RepoDraftReviewCounts.as_view(), name='api-v2.1-repo-draft-review-counts' ),
## user::activities
url(r'^api/v2.1/activities/$', ActivitiesView.as_view(), name='api-v2.1-acitvity'),