1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-02 15:38:15 +00:00

request reviewer api (#2472)

This commit is contained in:
C_Q
2018-11-05 10:33:33 +08:00
committed by Daniel Pan
parent 06695d2036
commit 1fd579fb44
21 changed files with 790 additions and 11 deletions

View File

@@ -4,6 +4,92 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@babel/helper-module-imports": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz",
"integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==",
"requires": {
"@babel/types": "^7.0.0"
}
},
"@babel/types": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.1.3.tgz",
"integrity": "sha512-RpPOVfK+yatXyn8n4PB1NW6k9qjinrXrRR8ugBN8fD6hCy5RXI6PSbVqpOJBO9oSaY7Nom4ohj35feb0UR9hSA==",
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.10",
"to-fast-properties": "^2.0.0"
},
"dependencies": {
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
}
}
},
"@emotion/babel-utils": {
"version": "0.6.10",
"resolved": "https://registry.npmjs.org/@emotion/babel-utils/-/babel-utils-0.6.10.tgz",
"integrity": "sha512-/fnkM/LTEp3jKe++T0KyTszVGWNKPNOUJfjNKLO17BzQ6QPxgbg3whayom1Qr2oLFH3V92tDymU+dT5q676uow==",
"requires": {
"@emotion/hash": "^0.6.6",
"@emotion/memoize": "^0.6.6",
"@emotion/serialize": "^0.9.1",
"convert-source-map": "^1.5.1",
"find-root": "^1.1.0",
"source-map": "^0.7.2"
},
"dependencies": {
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
}
}
},
"@emotion/hash": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.6.6.tgz",
"integrity": "sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ=="
},
"@emotion/memoize": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.6.6.tgz",
"integrity": "sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ=="
},
"@emotion/serialize": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.9.1.tgz",
"integrity": "sha512-zTuAFtyPvCctHBEL8KZ5lJuwBanGSutFEncqLn/m9T1a6a93smBStK+bZzcNPgj4QS8Rkw9VTwJGhRIUVO8zsQ==",
"requires": {
"@emotion/hash": "^0.6.6",
"@emotion/memoize": "^0.6.6",
"@emotion/unitless": "^0.6.7",
"@emotion/utils": "^0.8.2"
}
},
"@emotion/stylis": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.7.1.tgz",
"integrity": "sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ=="
},
"@emotion/unitless": {
"version": "0.6.7",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.6.7.tgz",
"integrity": "sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg=="
},
"@emotion/utils": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.8.2.tgz",
"integrity": "sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw=="
},
"@reach/router": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@reach/router/-/router-1.2.1.tgz",
@@ -172,6 +258,11 @@
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
"integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4="
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"accepts": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz",
@@ -869,6 +960,32 @@
"babel-types": "^6.26.0"
}
},
"babel-plugin-emotion": {
"version": "9.2.11",
"resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz",
"integrity": "sha512-dgCImifnOPPSeXod2znAmgc64NhaaOjGEHROR/M+lmStb3841yK1sgaDYAYMnlvWNz8GnpwIPN0VmNpbWYZ+VQ==",
"requires": {
"@babel/helper-module-imports": "^7.0.0",
"@emotion/babel-utils": "^0.6.4",
"@emotion/hash": "^0.6.2",
"@emotion/memoize": "^0.6.1",
"@emotion/stylis": "^0.7.0",
"babel-plugin-macros": "^2.0.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"convert-source-map": "^1.5.0",
"find-root": "^1.1.0",
"mkdirp": "^0.5.1",
"source-map": "^0.5.7",
"touch": "^2.0.1"
},
"dependencies": {
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
}
}
},
"babel-plugin-istanbul": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz",
@@ -884,6 +1001,58 @@
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-20.0.3.tgz",
"integrity": "sha1-r+3IU70/jcNUjqZx++adA8wsF2c="
},
"babel-plugin-macros": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.4.2.tgz",
"integrity": "sha512-NBVpEWN4OQ/bHnu1fyDaAaTPAjnhXCEPqr1RwqxrU7b6tZ2hypp+zX4hlNfmVGfClD5c3Sl6Hfj5TJNF5VG5aA==",
"requires": {
"cosmiconfig": "^5.0.5",
"resolve": "^1.8.1"
},
"dependencies": {
"cosmiconfig": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.6.tgz",
"integrity": "sha512-6DWfizHriCrFWURP1/qyhsiFvYdlJzbCzmtFWh744+KyWsJo5+kPzUZZaMRSSItoYc0pxFX7gEO7ZC1/gN/7AQ==",
"requires": {
"is-directory": "^0.3.1",
"js-yaml": "^3.9.0",
"parse-json": "^4.0.0"
}
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
},
"js-yaml": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz",
"integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
},
"parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
"requires": {
"error-ex": "^1.3.1",
"json-parse-better-errors": "^1.0.1"
}
},
"resolve": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
"integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==",
"requires": {
"path-parse": "^1.0.5"
}
}
}
},
"babel-plugin-react-transform": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/babel-plugin-react-transform/-/babel-plugin-react-transform-2.0.2.tgz",
@@ -962,8 +1131,7 @@
"babel-plugin-syntax-jsx": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=",
"dev": true
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
},
"babel-plugin-syntax-object-rest-spread": {
"version": "6.13.0",
@@ -2557,6 +2725,20 @@
"elliptic": "^6.0.0"
}
},
"create-emotion": {
"version": "9.2.12",
"resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-9.2.12.tgz",
"integrity": "sha512-P57uOF9NL2y98Xrbl2OuiDQUZ30GVmASsv5fbsjF4Hlraip2kyAvMm+2PoYUvFFw03Fhgtxk3RqZSm2/qHL9hA==",
"requires": {
"@emotion/hash": "^0.6.2",
"@emotion/memoize": "^0.6.1",
"@emotion/stylis": "^0.7.0",
"@emotion/unitless": "^0.6.2",
"csstype": "^2.5.2",
"stylis": "^3.5.0",
"stylis-rule-sheet": "^0.0.10"
}
},
"create-error-class": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz",
@@ -2860,6 +3042,11 @@
"cssom": "0.3.x"
}
},
"csstype": {
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.5.7.tgz",
"integrity": "sha512-Nt5VDyOTIIV4/nRFswoCKps1R5CD1hkiyjBE9/thNaNZILLEviVw9yWQw15+O+CpNjQKB/uvdcxFFOrSflY3Yw=="
},
"currently-unhandled": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
@@ -3240,6 +3427,15 @@
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
"integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k="
},
"emotion": {
"version": "9.2.12",
"resolved": "https://registry.npmjs.org/emotion/-/emotion-9.2.12.tgz",
"integrity": "sha512-hcx7jppaI8VoXxIWEhxpDW7I+B4kq9RNzQLmsrF6LY8BGKqe2N+gFAQr0EfuFucFlPs2A9HM4+xNj4NeqEWIOQ==",
"requires": {
"babel-plugin-emotion": "^9.2.11",
"create-emotion": "^9.2.12"
}
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@@ -4185,6 +4381,11 @@
"pkg-dir": "^2.0.0"
}
},
"find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"find-up": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
@@ -6672,6 +6873,11 @@
"integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==",
"dev": true
},
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="
},
"json-schema": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
@@ -7426,6 +7632,14 @@
"which": "^1.3.0"
}
},
"nopt": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
"integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=",
"requires": {
"abbrev": "1"
}
},
"normalize-package-data": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
@@ -9400,6 +9614,14 @@
"resolved": "https://registry.npmjs.org/react-immutable-proptypes/-/react-immutable-proptypes-2.1.0.tgz",
"integrity": "sha1-Aj1vObsVyXwHHp5g0A0TbqxfoLQ="
},
"react-input-autosize": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz",
"integrity": "sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA==",
"requires": {
"prop-types": "^15.5.8"
}
},
"react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
@@ -9437,6 +9659,20 @@
"babel-runtime": "^6.23.0"
}
},
"react-select": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-2.1.1.tgz",
"integrity": "sha512-ukie2LJStNfJEJ7wtqA+crAfzYpkpPr86urvmJGisECwsWJob9boCM4zjmKCi5QR7G8uY9+v7ZoliJpeCz/4xw==",
"requires": {
"classnames": "^2.2.5",
"emotion": "^9.1.2",
"memoize-one": "^4.0.0",
"prop-types": "^15.6.0",
"raf": "^3.4.0",
"react-input-autosize": "^2.2.1",
"react-transition-group": "^2.2.1"
}
},
"react-transform-catch-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/react-transform-catch-errors/-/react-transform-catch-errors-1.0.2.tgz",
@@ -10879,6 +11115,16 @@
"schema-utils": "^0.3.0"
}
},
"stylis": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.3.tgz",
"integrity": "sha512-TxU0aAscJghF9I3V9q601xcK3Uw1JbXvpsBGj/HULqexKOKlOEzzlIpLFRbKkCK990ccuxfXUqmPbIIo7Fq/cQ=="
},
"stylis-rule-sheet": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz",
"integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw=="
},
"supports-color": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz",
@@ -11092,6 +11338,14 @@
"resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.6.tgz",
"integrity": "sha1-wxdI5V0hDv/AD9zcfW5o19e7nOw="
},
"touch": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/touch/-/touch-2.0.2.tgz",
"integrity": "sha512-qjNtvsFXTRq7IuMLweVgFxmEuQ6gLbRs2jQxL80TtZ31dEKWYIxRXquij6w6VimyDek5hD3PytljHmEtAs2u0A==",
"requires": {
"nopt": "~1.0.10"
}
},
"tough-cookie": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz",

View File

@@ -24,6 +24,7 @@
"react-cookies": "^0.1.0",
"react-dom": "^16.5.2",
"react-moment": "^0.7.9",
"react-select": "^2.1.1",
"reactstrap": "^6.4.0",
"seafile-js": "^0.2.31",
"seafile-ui": "^0.1.10",

View File

@@ -0,0 +1,138 @@
import React from 'react';
import AsyncSelect from 'react-select/lib/Async';
import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import { seafileAPI } from '../../utils/seafile-api.js';
import '../../css/add-reviewer-dialog.css';
const propTypes = {
showReviewerDialog: PropTypes.bool.isRequired,
reviewID: PropTypes.string.isRequired,
toggleAddReviewerDialog: PropTypes.func.isRequired
};
class AddReviewerDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
reviewers: [],
selectedOption: null,
errorMsg: [],
};
this.Options = [];
}
componentWillMount() {
seafileAPI.listReviewers(this.props.reviewID).then((res) => {
this.setState({
reviewers: res.data.reviewers
});
});
}
handleSelectChange = (option) => {
this.setState({
selectedOption: option,
});
this.Options = [];
}
loadOptions = (value, callback) => {
if (value.trim().length > 0) {
this.Options = [];
let that = this;
seafileAPI.searchUsers(value.trim()).then((res) => {
for (let i = 0 ; i < res.data.users.length; i++) {
let obj = {};
obj.value = res.data.users[i].name;
obj.email = res.data.users[i].email;
obj.label =
<div>
<img src={res.data.users[i].avatar_url} className="avatar reviewer-select-avatar" alt=""/>
<span className='reviewer-select-name'>{res.data.users[i].name}</span>
</div>;
that.Options.push(obj);
}
callback(this.Options);
});
}
}
addReviewers = () => {
if (this.state.selectedOption.length > 0 ) {
let reviewers = [];
for (let i = 0; i < this.state.selectedOption.length; i ++) {
reviewers[i] = this.state.selectedOption[i].email;
}
seafileAPI.addReviewers(this.props.reviewID, reviewers).then((res) => {
if (res.data.failed.length > 0) {
let errorMsg = [];
for (let i = 0 ; i < res.data.failed.length ; i++) {
errorMsg[i] = res.data.failed[i];
}
this.setState({
errorMsg: errorMsg
});
let that = this;
setTimeout(() => {
that.setState({
errorMsg: []
});
}, 3000);
}
this.setState({
selectedOption: null
});
if (res.data.success.length > 0) {
this.listReviewers();
}
});
}
}
render() {
return (
<Modal isOpen={this.props.showReviewerDialog}>
<ModalHeader>{gettext('Request a review')}</ModalHeader>
<ModalBody >
<p>{gettext('Add new reviewer')}</p>
<AsyncSelect
className='reviewer-select' isMulti isFocused
loadOptions={this.loadOptions}
placeholder={gettext('Please enter 1 or more character')}
onChange={this.handleSelectChange}
/>
{this.state.errorMsg.length > 0 &&
this.state.errorMsg.map((item, index = 0, arr) => {
return (
<p className="error" key={index}>{this.state.errorMsg[index].email}
{':'}{this.state.errorMsg[index].error_msg}</p>
);
})
}
{ this.state.reviewers.length > 0 &&
this.state.reviewers.map((item, index = 0, arr) => {
return (
<div className="reviewer-select-info" key={index}>
<img className="avatar reviewer-select-avatar" src={item.avatar_url} alt=""/>
<span className="reviewer-select-name">{item.user_name}</span>
</div>
);
})
}
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.addReviewers}>{gettext('Submit')}</Button>
<Button color="secondary" onClick={this.props.toggleAddReviewerDialog}>
{gettext('Cancel')}</Button>
</ModalFooter>
</Modal>
);
}
}
AddReviewerDialog.propTypes = propTypes;
export default AddReviewerDialog;

View File

@@ -0,0 +1,13 @@
.reviewer-select-info {
margin-top: 10px;
}
.reviewer-select-avatar {
margin-right: 10px;
}
.reviewer-select-name {
height: 2em;
line-height: 2em;
}
.reviewer-select>div>div:nth-child(2) {
display: none;
}

View File

@@ -2,7 +2,7 @@
display: flex;
}
.header .common-list-btn {
.header .common-list-btn, .header .add-reviewer-btn {
margin-right: .25em;
}

View File

@@ -11,6 +11,7 @@ import Loading from './components/loading';
import Toast from './components/toast';
import ReviewComments from './components/review-list-view/review-comments';
import { Button, Tooltip } from 'reactstrap';
import AddReviewerDialog from './components/dialog/add-reviewer-dialog.js';
import 'seafile-ui';
import './assets/css/fa-solid.css';
@@ -37,6 +38,7 @@ class DraftReview extends React.Component {
commentWidth: 30,
isShowDiff: true,
showDiffTip: false,
showReviewerDialog: false,
};
}
@@ -153,6 +155,12 @@ class DraftReview extends React.Component {
});
}
toggleAddReviewerDialog = () => {
this.setState({
showReviewerDialog: !this.state.showReviewerDialog
});
}
componentWillMount() {
this.getCommentsNumber();
}
@@ -187,6 +195,8 @@ class DraftReview extends React.Component {
target="toggle-diff" toggle={this.toggleDiffTip}>
{gettext('View diff')}</Tooltip>
</div>
<button className="btn btn-primary add-reviewer-btn" onClick={this.toggleAddReviewerDialog}>
{gettext('Add reviewer')}</button>
<button className="btn btn-icon btn-secondary btn-active common-list-btn"
id="commentsNumber" type="button" data-active="false"
onMouseDown={this.toggleCommentList}>
@@ -244,6 +254,13 @@ class DraftReview extends React.Component {
}
</div>
</div>
{ this.state.showReviewerDialog &&
<AddReviewerDialog
showReviewerDialog={this.state.showReviewerDialog}
toggleAddReviewerDialog={this.toggleAddReviewerDialog}
reviewID={reviewID}
/>
}
</div>
);
}

View File

@@ -41,5 +41,6 @@ export const draftFileName = window.draftReview ? window.draftReview.config.draf
export const reviewID = window.draftReview ? window.draftReview.config.reviewID : '';
export const draftID = window.draftReview ? window.draftReview.config.draftID : '';
export const opStatus = window.draftReview ? window.draftReview.config.opStatus : '';
export const reviewPerm = window.draftReview ? window.draftReview.config.perm : '';
export const publishFileVersion = window.draftReview ? window.draftReview.config.publishFileVersion : '';
export const originFileVersion = window.draftReview ? window.draftReview.config.originFileVersion : '';

View File

@@ -0,0 +1,118 @@
# Copyright (c) 2012-2016 Seafile Ltd.
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 django.utils.translation import ugettext as _
from seaserv import seafile_api
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error, user_to_dict
from seahub.base.templatetags.seahub_tags import email2nickname
from seahub.base.accounts import User
from seahub.views import check_folder_permission
from seahub.utils import is_valid_username
from seahub.drafts.models import DraftReview, ReviewReviewer
from seahub.drafts.signals import request_reviewer_successful
class DraftReviewReviewerView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated, )
throttle_classes = (UserRateThrottle, )
def get(self, request, pk, format=None):
try:
r = DraftReview.objects.get(pk=pk)
except DraftReview.DoesNotExist:
return api_error(status.HTTP_404_NOT_FOUND,
'Review %s not found' % pk)
# format user result
try:
avatar_size = int(request.GET.get('avatar_size', 32))
except ValueError:
avatar_size = 32
# get reviewer list
reviewers = []
for x in r.reviewreviewer_set.all():
reviewer = user_to_dict(x.reviewer, request=request, avatar_size=avatar_size)
reviewers.append(reviewer)
return Response({'reviewers': reviewers})
def post(self, request, pk, format=None):
"""Create a draft review
"""
try:
r = DraftReview.objects.get(pk=pk)
except DraftReview.DoesNotExist:
return api_error(status.HTTP_404_NOT_FOUND,
'Review %s not found' % pk)
result = {}
result['failed'] = []
result['success'] = []
reviewers = request.data.getlist('reviewer')
for reviewer in reviewers:
if not is_valid_username(reviewer):
result['failed'].append({
'email': reviewer,
'error_msg': _(u'username invalid.')
})
continue
try:
User.objects.get(email=reviewer)
except User.DoesNotExist:
result['failed'].append({
'email': reviewer,
'error_msg': _(u'User %s not found.') % reviewer
})
continue
# can't share to owner
if reviewer == r.creator:
error_msg = _(u'Draft review can not be asked owner to review.')
result['failed'].append({
'email': reviewer,
'error_msg': error_msg
})
continue
# check perm
if seafile_api.check_permission_by_path(r.origin_repo_id, r.origin_file_path, reviewer) != 'rw':
error_msg = _(u'Permission denied.')
result['failed'].append({
'email': reviewer,
'error_msg': error_msg
})
continue
if ReviewReviewer.objects.filter(review_id=r, reviewer=reviewer):
error_msg = _(u'Reviewer %s has existed.') % reviewer
result['failed'].append({
'email': reviewer,
'error_msg': error_msg
})
continue
result['success'].append({
"user_info": {
"name": reviewer,
"nickname": email2nickname(reviewer)
}
})
ReviewReviewer.objects.add(reviewer, r)
request_reviewer_successful.send(sender=None, from_user=r.creator,
to_user=reviewer, review_id=r.id)
return Response(result)

View File

@@ -13,9 +13,12 @@ from seaserv import seafile_api
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error
from seahub.constants import PERMISSION_READ_WRITE
from seahub.views import check_folder_permission
from seahub.drafts.models import Draft, DraftReview, DraftReviewExist, \
DraftFileConflict
DraftFileConflict, ReviewReviewer
from seahub.drafts.signals import update_review_successful
class DraftReviewsView(APIView):
@@ -28,10 +31,10 @@ class DraftReviewsView(APIView):
"""
username = request.user.username
data = [x.to_dict() for x in DraftReview.objects.filter(creator=username)]
data += [x.review_id.to_dict() for x in ReviewReviewer.objects.filter(reviewer=username)]
return Response({'data': data})
def post(self, request, format=None):
"""Create a draft review
"""
@@ -74,10 +77,22 @@ class DraftReviewView(APIView):
return api_error(status.HTTP_404_NOT_FOUND,
'Review %s not found' % pk)
perm = check_folder_permission(request, r.origin_repo_id, r.origin_file_path)
# Review owner and 'rw' perm on the original file to close review
if st == 'closed':
if perm != PERMISSION_READ_WRITE or request.user.username != r.creator:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
r.status = st
r.save()
# Only 'rw' perm on original file can publish review
if st == 'finished':
if perm != PERMISSION_READ_WRITE:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
try:
d = Draft.objects.get(pk=r.draft_id_id)
@@ -119,9 +134,26 @@ class DraftReviewView(APIView):
# get draft published version
file_id = seafile_api.get_file_id_by_path(r.origin_repo_id, origin_file_path)
r.publish_file_version = file_id
r.status = st
r.save()
d.delete()
reviewers = ReviewReviewer.objects.filter(review_id=r)
# send notice to other reviewers if has
if reviewers:
for i in reviewers:
# If it is a reviewer operation, exclude it.
if i.reviewer == request.user.username:
continue
update_review_successful.send(sender=None, from_user=request.user.username,
to_user=i.reviewer, review_id=r.id, status=st)
# send notice to review owner
if request.user.username != r.creator:
update_review_successful.send(sender=None, from_user=request.user.username,
to_user=r.creator, review_id=r.id, status=st)
result = r.to_dict()
return Response(result)

View File

@@ -21,7 +21,6 @@ from seahub.api2.authentication import TokenAuthentication
from seahub.api2.endpoints.utils import add_org_context
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error
from seahub.constants import PERMISSION_READ_WRITE
from seahub.drafts.models import Draft, DraftFileExist, DraftFileConflict
from seahub.views import check_folder_permission
from seahub.utils import gen_file_get_url
@@ -63,7 +62,7 @@ class DraftsView(APIView):
# perm check
perm = check_folder_permission(request, repo.id, file_path)
if perm != PERMISSION_READ_WRITE:
if perm is None:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.15 on 2018-10-23 08:58
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import seahub.base.fields
class Migration(migrations.Migration):
dependencies = [
('drafts', '0003_reviewcomment'),
]
operations = [
migrations.CreateModel(
name='ReviewReviewer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
('reviewer', seahub.base.fields.LowerCaseCharField(db_index=True, max_length=255)),
('review_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='drafts.DraftReview')),
],
options={
'ordering': ['-created_at', '-updated_at'],
'abstract': False,
},
),
]

View File

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.15 on 2018-10-24 10:09
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('drafts', '0004_reviewreviewer'),
]
operations = [
migrations.AlterModelOptions(
name='reviewreviewer',
options={},
),
migrations.RemoveField(
model_name='reviewreviewer',
name='created_at',
),
migrations.RemoveField(
model_name='reviewreviewer',
name='updated_at',
),
]

View File

@@ -243,3 +243,27 @@ class ReviewComment(TimestampedModel):
'resolved': self.resolved,
'detail': self.detail,
}
class ReviewReviewerManager(models.Manager):
def add(self, reviewer, review_id):
review_reviewer = self.model(reviewer=reviewer, review_id=review_id)
review_reviewer.save(using=self._db)
return review_reviewer
class ReviewReviewer(models.Model):
"""
Model used to record review reviewer.
"""
reviewer = LowerCaseCharField(max_length=255, db_index=True)
review_id = models.ForeignKey('DraftReview', on_delete=models.CASCADE)
objects = ReviewReviewerManager()
def to_dict(self):
return {
'nickname': email2nickname(self.reviewer),
'name': self.reviewer,
}

View File

@@ -2,3 +2,5 @@
import django.dispatch
comment_review_successful = django.dispatch.Signal(providing_args=["review", "comment", "author"])
request_reviewer_successful = django.dispatch.Signal(providing_args=["from_user", "to_user", "review_id"])
update_review_successful = django.dispatch.Signal(providing_args=["from_user", "to_user", "review_id", "status"])

View File

@@ -45,5 +45,6 @@ def review(request, pk):
"draft_file_name": draft_file_name,
"origin_file_version": d_r.origin_file_version,
"publish_file_version": d_r.publish_file_version,
"status": d_r.status
"status": d_r.status,
"permission": permission
})

View File

@@ -54,6 +54,8 @@ MSG_TYPE_REPO_SHARE_TO_GROUP = 'repo_share_to_group'
MSG_TYPE_USER_MESSAGE = 'user_message'
MSG_TYPE_FILE_COMMENT = 'file_comment'
MSG_TYPE_REVIEW_COMMENT = 'review_comment'
MSG_TYPE_UPDATE_REVIEW = 'update_review'
MSG_TYPE_REQUEST_REVIEWER = 'request_reviewer'
MSG_TYPE_GUEST_INVITATION_ACCEPTED = 'guest_invitation_accepted'
USER_NOTIFICATION_COUNT_CACHE_PREFIX = 'USER_NOTIFICATION_COUNT_'
@@ -98,6 +100,17 @@ def review_comment_msg_to_json(review_id, author, comment):
'author': author,
'comment': comment})
def request_reviewer_msg_to_json(review_id, from_user, to_user):
return json.dumps({'review_id': review_id,
'from_user': from_user,
'to_user': to_user})
def update_review_msg_to_json(review_id, from_user, to_user, status):
return json.dumps({'review_id': review_id,
'from_user': from_user,
'to_user': to_user,
'status': status})
def guest_invitation_accepted_msg_to_json(invitation_id):
return json.dumps({'invitation_id': invitation_id})
@@ -298,6 +311,16 @@ class UserNotificationManager(models.Manager):
"""
return self._add_user_notification(to_user, MSG_TYPE_REVIEW_COMMENT, detail)
def add_request_reviewer_msg(self, to_user, detail):
"""Notify ``to_user`` that reviewer
"""
return self._add_user_notification(to_user, MSG_TYPE_REQUEST_REVIEWER, detail)
def add_update_review_msg(self, to_user, detail):
"""Notify ``to_user`` that reviewer and owner
"""
return self._add_user_notification(to_user, MSG_TYPE_UPDATE_REVIEW, detail)
def add_guest_invitation_accepted_msg(self, to_user, detail):
"""Nofity ``to_user`` that a guest has accpeted an invitation.
"""
@@ -399,6 +422,12 @@ class UserNotification(models.Model):
def is_review_comment_msg(self):
return self.msg_type == MSG_TYPE_REVIEW_COMMENT
def is_request_reviewer_msg(self):
return self.msg_type == MSG_TYPE_REQUEST_REVIEWER
def is_update_review_msg(self):
return self.msg_type == MSG_TYPE_UPDATE_REVIEW
def is_guest_invitation_accepted_msg(self):
return self.msg_type == MSG_TYPE_GUEST_INVITATION_ACCEPTED
@@ -760,6 +789,49 @@ class UserNotification(models.Model):
}
return msg
def format_request_reviewer_msg(self):
try:
d = json.loads(self.detail)
except Exception as e:
logger.error(e)
return _(u"Internal error")
review_id = d['review_id']
from_user = d['from_user']
msg = _("%(from_user)s has sent you a request for review <a href='%(file_url)s'>%(review_id)s</a>") % {
'review_id': review_id,
'file_url': reverse('drafts:review', args=[review_id]),
'from_user': escape(email2nickname(from_user))
}
return msg
def format_update_review_msg(self):
try:
d = json.loads(self.detail)
except Exception as e:
logger.error(e)
return _(u"Internal error")
review_id = d['review_id']
from_user = d['from_user']
status = d['status']
if status == 'closed':
msg = _("%(from_user)s has closed review <a href='%(file_url)s'>%(review_id)s</a>") % {
'review_id': review_id,
'file_url': reverse('drafts:review', args=[review_id]),
'from_user': escape(email2nickname(from_user))
}
if status == 'finished':
msg = _("%(from_user)s has published review <a href='%(file_url)s'>%(review_id)s</a>") % {
'review_id': review_id,
'file_url': reverse('drafts:review', args=[review_id]),
'from_user': escape(email2nickname(from_user))
}
return msg
def format_guest_invitation_accepted_msg(self):
try:
d = json.loads(self.detail)
@@ -794,7 +866,8 @@ from seahub.group.signals import grpmsg_added, group_join_request, add_user_to_g
from seahub.share.signals import share_repo_to_user_successful, \
share_repo_to_group_successful
from seahub.invitations.signals import accept_guest_invitation_successful
from seahub.drafts.signals import comment_review_successful
from seahub.drafts.signals import comment_review_successful, \
request_reviewer_successful, update_review_successful
@receiver(upload_file_successful)
def add_upload_file_msg_cb(sender, **kwargs):
@@ -908,6 +981,26 @@ def comment_review_successful_cb(sender, **kwargs):
detail = review_comment_msg_to_json(review.id, author, comment)
UserNotification.objects.add_review_comment_msg(review.creator, detail)
@receiver(request_reviewer_successful)
def requeset_reviewer_successful_cb(sender, **kwargs):
from_user = kwargs['from_user']
review_id = kwargs['review_id']
to_user = kwargs['to_user']
detail = request_reviewer_msg_to_json(review_id, from_user, to_user)
UserNotification.objects.add_request_reviewer_msg(to_user, detail)
@receiver(update_review_successful)
def update_review_successful_cb(sender, **kwargs):
from_user = kwargs['from_user']
review_id = kwargs['review_id']
to_user = kwargs['to_user']
status = kwargs['status']
detail = update_review_msg_to_json(review_id, from_user, to_user, status)
UserNotification.objects.add_update_review_msg(to_user, detail)
@receiver(accept_guest_invitation_successful)
def accept_guest_invitation_successful_cb(sender, **kwargs):

View File

@@ -38,6 +38,12 @@
{% elif notice.is_review_comment_msg %}
<p class="brief">{{ notice.format_review_comment_msg|safe }}</p>
{% elif notice.is_update_review_msg %}
<p class="brief">{{ notice.format_update_review_msg|safe }}</p>
{% elif notice.is_request_reviewer_msg %}
<p class="brief">{{ notice.format_request_reviewer_msg|safe }}</p>
{% elif notice.is_guest_invitation_accepted_msg %}
<p class="brief">{{ notice.format_guest_invitation_accepted_msg|safe }}</p>

View File

@@ -192,5 +192,18 @@ def add_notice_from_info(notices):
except Exception as e:
logger.error(e)
elif notice.is_request_reviewer_msg():
try:
d = json.loads(notice.detail)
notice.msg_from = d['from_user']
except Exception as e:
logger.error(e)
elif notice.is_update_review_msg():
try:
d = json.loads(notice.detail)
notice.msg_from = d['from_user']
except Exception as e:
logger.error(e)
return notices

View File

@@ -13,6 +13,7 @@
draftOriginRepoID: '{{ draft_origin_repo_id }}',
draftFileName: '{{ draft_file_name }}',
opStatus: '{{ status }}',
perm: '{{ permission }}',
publishFileVersion: '{{ publish_file_version }}',
originFileVersion: '{{ origin_file_version }}',
}

View File

@@ -40,6 +40,12 @@
{% elif notice.is_review_comment_msg %}
<p class="brief">{{ notice.format_review_comment_msg|safe }}</p>
{% elif notice.is_update_review_msg %}
<p class="brief">{{ notice.format_update_review_msg|safe }}</p>
{% elif notice.is_request_reviewer_msg %}
<p class="brief">{{ notice.format_request_reviewer_msg|safe }}</p>
{% elif notice.is_guest_invitation_accepted_msg %}
<p class="brief">{{ notice.format_guest_invitation_accepted_msg|safe }}</p>

View File

@@ -68,6 +68,7 @@ from seahub.api2.endpoints.user_avatar import UserAvatarView
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.activities import ActivitiesView
from seahub.api2.endpoints.wiki_pages import WikiPageView, WikiPagesView, WikiPagesDirView, WikiPageContentView
from seahub.api2.endpoints.revision_tag import TaggedItemsView, TagNamesView
@@ -340,6 +341,7 @@ urlpatterns = [
## user::reviews
url(r'^api/v2.1/reviews/$', DraftReviewsView.as_view(), name='api-v2.1-draft-reviews'),
url(r'^api/v2.1/review/(?P<pk>\d+)/$', DraftReviewView.as_view(), name='api-v2.1-draft-review'),
url(r'^api/v2.1/review/(?P<pk>\d+)/reviewer/$', DraftReviewReviewerView.as_view(), name='api-v2.1-draft-review-reviewer'),
## user::activities
url(r'^api/v2.1/activities/$', ActivitiesView.as_view(), name='api-v2.1-acitvity'),