mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-12 21:30:39 +00:00
Change participants UI and API (#3889)
* change tooltip ID * change dialog API * change mention style * change comment panel API change detail panel API * change Tooltip * change mention color update seafile-api change comment content click
This commit is contained in:
6
frontend/package-lock.json
generated
6
frontend/package-lock.json
generated
@@ -16272,9 +16272,9 @@
|
||||
}
|
||||
},
|
||||
"seafile-js": {
|
||||
"version": "0.2.108",
|
||||
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.108.tgz",
|
||||
"integrity": "sha512-uGBqFfGxywsf4z+I4UX8Prz4xJe46Gvy/aCRed3751B0fCUG3+OSiKtJXnZH9TN3lOVp4sJpm7MkER1Bf1ln9A==",
|
||||
"version": "0.2.109",
|
||||
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.109.tgz",
|
||||
"integrity": "sha512-qcFKdC8hA1G3Mbe4PpJ/ircBD9Miaduur2qIPkjyYIzJ8MvownGxku+r13UsNh6fLp3oCs2GqzuY4UDO5y1gcw==",
|
||||
"requires": {
|
||||
"axios": "^0.18.0",
|
||||
"form-data": "^2.3.2",
|
||||
|
@@ -38,7 +38,7 @@
|
||||
"react-responsive": "^6.1.2",
|
||||
"react-select": "^2.4.1",
|
||||
"reactstrap": "^6.4.0",
|
||||
"seafile-js": "^0.2.108",
|
||||
"seafile-js": "^0.2.109",
|
||||
"socket.io-client": "^2.2.0",
|
||||
"sw-precache-webpack-plugin": "0.11.4",
|
||||
"unified": "^7.0.0",
|
||||
|
@@ -89,14 +89,12 @@ class FileParticipantDialog extends Component {
|
||||
if (!selectedOption || selectedOption.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < selectedOption.length; i++) {
|
||||
seafileAPI.addFileParticipant(repoID, filePath, selectedOption[i].email).then((res) => {
|
||||
let emails = selectedOption.map((option) => {return option.email;});
|
||||
seafileAPI.addFileParticipants(repoID, filePath, emails).then((res) => {
|
||||
this.props.onParticipantsChange(repoID, filePath);
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
toaster.danger(Utils.getErrorMsg(error));
|
||||
});
|
||||
}
|
||||
this.setState({ selectedOption: null });
|
||||
this.refs.userSelect.clearSelect();
|
||||
};
|
||||
|
@@ -28,6 +28,7 @@ class DetailCommentList extends React.Component {
|
||||
relatedUsers: null,
|
||||
comment: '',
|
||||
};
|
||||
this.toBeAddedParticipant = [];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -55,21 +56,33 @@ class DetailCommentList extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
submitComment = () => {
|
||||
let comment = this.state.comment;
|
||||
addComment = () => {
|
||||
const { repoID, filePath } = this.props;
|
||||
if (comment.trim()) {
|
||||
seafileAPI.postComment(repoID, filePath, comment.trim()).then(() => {
|
||||
if (!this.state.comment.trim()) return;
|
||||
seafileAPI.postComment(repoID, filePath, this.state.comment.trim()).then(() => {
|
||||
this.listComments();
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
}).catch(err => {
|
||||
toaster.danger(Utils.getErrorMsg(err));
|
||||
});
|
||||
this.addParticipant(username);
|
||||
}
|
||||
this.setState({ comment: '' });
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
this.addParticipant(username);
|
||||
if (this.toBeAddedParticipant.length === 0) {
|
||||
this.addComment();
|
||||
} else {
|
||||
const { repoID, filePath } = this.props;
|
||||
seafileAPI.addFileParticipants(repoID, filePath, this.toBeAddedParticipant).then((res) => {
|
||||
this.props.onParticipantsChange(repoID, filePath);
|
||||
this.toBeAddedParticipant = [];
|
||||
this.addComment();
|
||||
}).catch((err) => {
|
||||
toaster.danger(Utils.getErrorMsg(err));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resolveComment = (event) => {
|
||||
const { repoID } = this.props;
|
||||
seafileAPI.updateComment(repoID, event.target.id, 'true').then(() => {
|
||||
@@ -104,7 +117,7 @@ class DetailCommentList extends React.Component {
|
||||
const { repoID } = this.props;
|
||||
seafileAPI.listRepoRelatedUsers(repoID).then((res) => {
|
||||
let users = res.data.user_list.map((item) => {
|
||||
return { id: (siteRoot + 'profile/' + item.email), display: item.name};
|
||||
return { id: item.email, display: item.name};
|
||||
});
|
||||
this.setState({ relatedUsers: users });
|
||||
});
|
||||
@@ -116,23 +129,13 @@ class DetailCommentList extends React.Component {
|
||||
|
||||
addParticipant = (email) => {
|
||||
if (this.checkParticipant(email)) return;
|
||||
const { repoID, filePath } = this.props;
|
||||
seafileAPI.addFileParticipant(repoID, filePath, email).then((res) => {
|
||||
this.props.onParticipantsChange(repoID, filePath);
|
||||
}).catch((err) => {
|
||||
toaster.danger(Utils.getErrorMsg(err));
|
||||
});
|
||||
this.toBeAddedParticipant.push(email);
|
||||
}
|
||||
|
||||
renderUserSuggestion = (entry, search, highlightedDisplay, index, focused) => {
|
||||
return <div className={`user ${focused ? 'focused' : ''}`}>{highlightedDisplay}</div>;
|
||||
}
|
||||
|
||||
handleMention = (id, display) => {
|
||||
const email = id.slice(id.lastIndexOf('/') + 1);
|
||||
this.addParticipant(email);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { repoID, filePath, fileParticipantList } = this.props;
|
||||
return (
|
||||
@@ -173,13 +176,14 @@ class DetailCommentList extends React.Component {
|
||||
>
|
||||
<Mention
|
||||
trigger="@"
|
||||
displayTransform={(username, display) => `@${display}`}
|
||||
data={this.state.relatedUsers}
|
||||
renderSuggestion={this.renderUserSuggestion}
|
||||
style={defaultMentionStyle}
|
||||
onAdd={this.handleMention}
|
||||
onAdd={(id, display) => {this.addParticipant(id);}}
|
||||
/>
|
||||
</MentionsInput>
|
||||
<Button className="submit-comment" color="primary" size="sm" onClick={this.submitComment}>{gettext('Submit')}</Button>
|
||||
<Button className="submit-comment" color="primary" size="sm" onClick={this.onSubmit}>{gettext('Submit')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -243,6 +247,15 @@ class CommentItem extends React.Component {
|
||||
this.setState({ newComment: event.target.value });
|
||||
}
|
||||
|
||||
onCommentClick = (e) => {
|
||||
// click participant link, page shouldn't jump
|
||||
if (e.target.nodeName !== 'A') return;
|
||||
const preNode = e.target.previousSibling;
|
||||
if (preNode && preNode.nodeType === 3 && preNode.nodeValue.slice(-1) === '@') {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
renderInfo = (item) => {
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -293,7 +306,11 @@ class CommentItem extends React.Component {
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className="seafile-comment-content" dangerouslySetInnerHTML={{ __html: this.state.html }}></div>
|
||||
<div
|
||||
className="seafile-comment-content"
|
||||
dangerouslySetInnerHTML={{ __html: this.state.html }}
|
||||
onClick={e => this.onCommentClick(e)}
|
||||
></div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
@@ -8,7 +8,6 @@ import { seafileAPI } from '../../utils/seafile-api';
|
||||
import ParticipantsList from './participants-list';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import toaster from '../toast';
|
||||
import { siteRoot } from '../../utils/constants';
|
||||
import { MentionsInput, Mention } from 'react-mentions';
|
||||
import { defaultStyle, defaultMentionStyle } from '../../css/react-mentions-default-style';
|
||||
import '../../css/comments-list.css';
|
||||
@@ -33,6 +32,7 @@ class CommentPanel extends React.Component {
|
||||
relatedUsers: null,
|
||||
comment: '',
|
||||
};
|
||||
this.toBeAddedParticipant = [];
|
||||
}
|
||||
|
||||
toggleResolvedComment = () => {
|
||||
@@ -53,7 +53,7 @@ class CommentPanel extends React.Component {
|
||||
listRepoRelatedUsers = () => {
|
||||
seafileAPI.listRepoRelatedUsers(repoID).then((res) => {
|
||||
let users = res.data.user_list.map((item) => {
|
||||
return { id: (siteRoot + 'profile/' + item.email), display: item.name};
|
||||
return { id: item.email, display: item.name};
|
||||
});
|
||||
this.setState({ relatedUsers: users });
|
||||
});
|
||||
@@ -63,20 +63,31 @@ class CommentPanel extends React.Component {
|
||||
this.setState({ comment: event.target.value });
|
||||
}
|
||||
|
||||
submitComment = () => {
|
||||
let comment = this.state.comment;
|
||||
if (comment.trim()) {
|
||||
seafileAPI.postComment(repoID, filePath, comment).then(() => {
|
||||
addComment = () => {
|
||||
if (!this.state.comment.trim()) return;
|
||||
seafileAPI.postComment(repoID, filePath, this.state.comment.trim()).then(() => {
|
||||
this.listComments();
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
}).catch(err => {
|
||||
toaster.danger(Utils.getErrorMsg(err));
|
||||
});
|
||||
this.addParticipant(username);
|
||||
}
|
||||
this.setState({ comment: '' });
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
this.addParticipant(username);
|
||||
if (this.toBeAddedParticipant.length === 0) {
|
||||
this.addComment();
|
||||
} else {
|
||||
seafileAPI.addFileParticipants(repoID, filePath, this.toBeAddedParticipant).then((res) => {
|
||||
this.onParticipantsChange(repoID, filePath);
|
||||
this.toBeAddedParticipant = [];
|
||||
this.addComment();
|
||||
}).catch((err) => {
|
||||
toaster.danger(Utils.getErrorMsg(err));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resolveComment = (event) => {
|
||||
seafileAPI.updateComment(repoID, event.target.id, 'true').then(() => {
|
||||
this.listComments();
|
||||
@@ -128,23 +139,13 @@ class CommentPanel extends React.Component {
|
||||
|
||||
addParticipant = (email) => {
|
||||
if (this.checkParticipant(email)) return;
|
||||
seafileAPI.addFileParticipant(repoID, filePath, email).then((res) => {
|
||||
this.onParticipantsChange(repoID, filePath);
|
||||
}).catch((err) => {
|
||||
let errMessage = Utils.getErrorMsg(err);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
this.toBeAddedParticipant.push(email);
|
||||
}
|
||||
|
||||
renderUserSuggestion = (entry, search, highlightedDisplay, index, focused) => {
|
||||
return <div className={`user ${focused ? 'focused' : ''}`}>{highlightedDisplay}</div>;
|
||||
}
|
||||
|
||||
handleMention = (id, display) => {
|
||||
const email = id.slice(id.lastIndexOf('/') + 1);
|
||||
this.addParticipant(email);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.listComments();
|
||||
this.getParticipants();
|
||||
@@ -221,15 +222,14 @@ class CommentPanel extends React.Component {
|
||||
>
|
||||
<Mention
|
||||
trigger="@"
|
||||
displayTransform={(username, display) => `@${display}`}
|
||||
data={this.state.relatedUsers}
|
||||
renderSuggestion={this.renderUserSuggestion}
|
||||
style={defaultMentionStyle}
|
||||
onAdd={this.handleMention}
|
||||
onAdd={(id, display) => {this.addParticipant(id);}}
|
||||
/>
|
||||
</MentionsInput>
|
||||
<Button
|
||||
className="submit-comment" color="primary"
|
||||
size="sm" onClick={this.submitComment} >
|
||||
<Button className="submit-comment" color="primary" size="sm" onClick={this.onSubmit}>
|
||||
{gettext('Submit')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -268,15 +268,11 @@ class CommentItem extends React.Component {
|
||||
}
|
||||
|
||||
convertComment = (mdFile) => {
|
||||
processor.process(mdFile).then(
|
||||
(result) => {
|
||||
processor.process(mdFile).then((result) => {
|
||||
let html = String(result);
|
||||
this.setState({
|
||||
html: html
|
||||
this.setState({ html: html });
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
toggleEditComment = () => {
|
||||
this.setState({
|
||||
@@ -298,6 +294,15 @@ class CommentItem extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
onCommentClick = (e) => {
|
||||
// click participant link, page shouldn't jump
|
||||
if (e.target.nodeName !== 'A') return;
|
||||
const preNode = e.target.previousSibling;
|
||||
if (preNode && preNode.nodeType === 3 && preNode.nodeValue.slice(-1) === '@') {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.convertComment(this.props.item.comment);
|
||||
}
|
||||
@@ -324,7 +329,7 @@ class CommentItem extends React.Component {
|
||||
<div className="seafile-edit-comment">
|
||||
<textarea className="edit-comment-input" value={this.state.newComment} onChange={this.handleCommentChange} clos="100" rows="3" warp="virtual"></textarea>
|
||||
<Button className="comment-btn" color="success" size="sm" onClick={this.updateComment} id={item.id}>{gettext('Update')}</Button>{' '}
|
||||
<Button className="comment-btn" color="secondary" size="sm" onClick={this.toggleEditComment}> {gettext('Cancel')}</Button>
|
||||
<Button className="comment-btn" color="secondary" size="sm" onClick={this.toggleEditComment}>{gettext('Cancel')}</Button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
@@ -344,24 +349,25 @@ class CommentItem extends React.Component {
|
||||
<i className="fas fa-ellipsis-v"></i>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
{
|
||||
(item.user_email === username) &&
|
||||
{(item.user_email === username) &&
|
||||
<DropdownItem onClick={this.props.deleteComment} className="delete-comment"
|
||||
id={item.id}>{gettext('Delete')}</DropdownItem>}
|
||||
{
|
||||
(item.user_email === username) &&
|
||||
{(item.user_email === username) &&
|
||||
<DropdownItem onClick={this.toggleEditComment}
|
||||
className="edit-comment" id={item.id}>{gettext('Edit')}</DropdownItem>
|
||||
}
|
||||
{
|
||||
!item.resolved &&
|
||||
{!item.resolved &&
|
||||
<DropdownItem onClick={this.props.resolveComment} className="seafile-comment-resolved"
|
||||
id={item.id}>{gettext('Mark as resolved')}</DropdownItem>
|
||||
}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className="seafile-comment-content" dangerouslySetInnerHTML={{ __html: this.state.html }}></div>
|
||||
<div
|
||||
className="seafile-comment-content"
|
||||
dangerouslySetInnerHTML={{ __html: this.state.html }}
|
||||
onClick={e => this.onCommentClick(e)}
|
||||
></div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
@@ -22,6 +22,7 @@ class ParticipantsList extends React.Component {
|
||||
showDialog : false,
|
||||
tooltipOpen: false,
|
||||
};
|
||||
this.targetID = 'add-participant-icon';
|
||||
}
|
||||
|
||||
toggleDialog = () => {
|
||||
@@ -32,6 +33,10 @@ class ParticipantsList extends React.Component {
|
||||
this.setState({ tooltipOpen: !this.state.tooltipOpen });
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.targetID = this.targetID + Math.floor(Math.random() * 1000);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { participants, repoID, filePath, showIconTip } = this.props;
|
||||
return (
|
||||
@@ -39,11 +44,11 @@ class ParticipantsList extends React.Component {
|
||||
{participants.map((item, index) => {
|
||||
return <Participant item={item} index={index} key={index}/>;
|
||||
})}
|
||||
<span className="add-participants" onClick={this.toggleDialog} id="add-participant-icon">
|
||||
<span className="add-participants" onClick={this.toggleDialog} id={this.targetID}>
|
||||
<i className="fas fa-plus-circle"></i>
|
||||
</span>
|
||||
{showIconTip &&
|
||||
<Tooltip toggle={this.tooltipToggle} delay={{show: 0, hide: 0}} target="add-participant-icon" placement='bottom' isOpen={this.state.tooltipOpen}>
|
||||
<Tooltip toggle={this.tooltipToggle} delay={{show: 0, hide: 0}} target={this.targetID} placement='bottom' isOpen={this.state.tooltipOpen}>
|
||||
{gettext('Add participants')}
|
||||
</Tooltip>
|
||||
}
|
||||
@@ -77,19 +82,23 @@ class Participant extends React.Component {
|
||||
this.state = {
|
||||
showAvatarTooltip: false,
|
||||
};
|
||||
this.targetID = 'participant-avatar-';
|
||||
}
|
||||
|
||||
toggleAvatarTooltip = () => {
|
||||
this.setState({ showAvatarTooltip: !this.state.showAvatarTooltip });
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.targetID = this.targetID + this.props.index + Math.floor(Math.random() * 1000);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { item, index } = this.props;
|
||||
const target = 'participant-avatar-' + index;
|
||||
return (
|
||||
<span className="participant-avatar">
|
||||
<img src={item.avatar_url} className="avatar" id={target} alt="avatar" key={index}/>
|
||||
<Tooltip toggle={this.toggleAvatarTooltip} delay={{show: 0, hide: 0}} target={target} placement='bottom' isOpen={this.state.showAvatarTooltip}>
|
||||
<img src={item.avatar_url} className="avatar" id={this.targetID} alt="avatar" key={index}/>
|
||||
<Tooltip toggle={this.toggleAvatarTooltip} delay={{show: 0, hide: 0}} target={this.targetID} placement='bottom' isOpen={this.state.showAvatarTooltip}>
|
||||
{item.name}
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
22
frontend/src/css/react-mentions-default-style.js
vendored
22
frontend/src/css/react-mentions-default-style.js
vendored
@@ -26,20 +26,24 @@ const defaultStyle = {
|
||||
},
|
||||
'&multiLine': {
|
||||
control: {
|
||||
fontFamily: 'monospace',
|
||||
border: '1px solid silver',
|
||||
},
|
||||
highlighter: {
|
||||
padding: 9,
|
||||
},
|
||||
input: {
|
||||
padding: 5,
|
||||
padding: '8px 6px',
|
||||
minHeight: 90,
|
||||
height: 90,
|
||||
outline: 0,
|
||||
border: '1px solid #e6e6dd',
|
||||
backgroundColor: '#fff',
|
||||
overfflowY: 'auto'
|
||||
borderRadius: '5px',
|
||||
overfflowY: 'auto',
|
||||
'&focused': {
|
||||
backgroundColor: '#cee4e5',
|
||||
outlineOffset: '-2px',
|
||||
outlineColor: '-webkit-focus-ring-color',
|
||||
outlineStyle: 'auto',
|
||||
outlineWidth: '5px',
|
||||
},
|
||||
},
|
||||
},
|
||||
suggestions: {
|
||||
@@ -54,16 +58,18 @@ const defaultStyle = {
|
||||
},
|
||||
item: {
|
||||
padding: '5px 15px',
|
||||
minWidth: '50px',
|
||||
borderBottom: '1px solid rgba(0,0,0,0.15)',
|
||||
'&focused': {
|
||||
backgroundColor: '#cee4e5',
|
||||
backgroundColor: '#f19654',
|
||||
color: '#fff',
|
||||
fontWeight: '700',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const defaultMentionStyle = {
|
||||
backgroundColor: '#cee4e5'
|
||||
};
|
||||
|
||||
export { defaultStyle, defaultMentionStyle };
|
||||
|
Reference in New Issue
Block a user