Skip to content

Commit

Permalink
Fix Bug 1603610: Enable users to leave comments on translations (#1524)
Browse files Browse the repository at this point in the history
Note: The feature is currently disabled in the UI.
  • Loading branch information
abowler2 authored and mathjazz committed Jan 24, 2020
1 parent 6b39ab8 commit 597c496
Show file tree
Hide file tree
Showing 38 changed files with 985 additions and 215 deletions.
23 changes: 23 additions & 0 deletions frontend/public/static/locale/en-US/translate.ftl
Expand Up @@ -81,6 +81,13 @@ resourceprogress-ResourceProgress--errors = Errors
resourceprogress-ResourceProgress--missing = Missing
## Comments
## Allows user to leave comments on translations

comments-AddComment--input =
.placeholder = Write a comment…
## Editor Menu
## Allows contributors to modify or propose a translation

Expand Down Expand Up @@ -270,6 +277,15 @@ history-Translation--button-not-rejected =
history-Translation--button-rejected =
.title = Rejected
history-Translation--button-comment = Comment
.title = Toggle translation comments
history-Translation--button-comments = { $commentCount ->
[one] { $commentCount } Comment
*[other] { $commentCount } Comments
}
.title = Toggle translation comments
## Interactive Tour
## Shows an interactive Tour on the "Tutorial" project,
Expand Down Expand Up @@ -436,6 +452,7 @@ notification--make-suggestions-enabled = Make Suggestions enabled
notification--make-suggestions-disabled = Make Suggestions disabled
notification--entity-not-found = Can’t load specified string
notification--string-link-copied = Link copied to clipboard
notification--comment-added = Comment added
## OtherLocales Translation
Expand Down Expand Up @@ -567,6 +584,12 @@ search-TimeRangeFilter--edit-range = <glyph></glyph>Edit Range
search-TimeRangeFilter--save-range = Save Range
## User Avatar
## Shows user Avatar with alt text

user-UserAvatar--anon-alt-text = Anonymous User
user-UserAvatar--alt-text = User Profile
## User Menu
## Shows user menu entries and options to sign in or out.

Expand Down
32 changes: 32 additions & 0 deletions frontend/src/core/api/base.js
Expand Up @@ -35,6 +35,18 @@ export default class APIBase {
return new URL(url, window.location.origin);
}

toCamelCase = (s: string) => {
return s.replace(/([-_][a-z])/ig, ($1) => {
return $1.toUpperCase()
.replace('-', '')
.replace('_', '');
});
}

isObject = function (obj: any) {
return obj === Object(obj) && !Array.isArray(obj) && typeof obj !== 'function';
}

async fetch(
url: string,
method: string,
Expand Down Expand Up @@ -87,4 +99,24 @@ export default class APIBase {
return {};
}
}

keysToCamelCase(results: any) {
if (this.isObject(results)) {
const newObj: any = {};

Object.keys(results)
.forEach((key) => {
newObj[this.toCamelCase(key)] = this.keysToCamelCase(results[key]);
});

return newObj;
}
else if (Array.isArray(results)) {
return results.map((i) => {
return this.keysToCamelCase(i);
});
}

return results;
}
}
19 changes: 19 additions & 0 deletions frontend/src/core/api/comment.js
@@ -0,0 +1,19 @@
/* @flow */

import APIBase from './base';


export default class CommentAPI extends APIBase {
add(comment: string, translationId: number) {
const payload = new URLSearchParams();
payload.append('comment', comment);
payload.append('translationId', translationId.toString());

const headers = new Headers();
const csrfToken = this.getCSRFToken();
headers.append('X-Requested-With', 'XMLHttpRequest');
headers.append('X-CSRFToken', csrfToken);

return this.fetch('/add-comment/', 'POST', payload, headers);
}
}
4 changes: 3 additions & 1 deletion frontend/src/core/api/entity.js
Expand Up @@ -125,7 +125,9 @@ export default class EntityAPI extends APIBase {
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');

return await this.fetch('/get-history/', 'GET', payload, headers);
const results = await this.fetch('/get-history/', 'GET', payload, headers);

return this.keysToCamelCase(results);
}

async getOtherLocales(
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/core/api/index.js
Expand Up @@ -9,12 +9,14 @@ import ProjectAPI from './project';
import ResourceAPI from './resource';
import TranslationAPI from './translation';
import UserAPI from './user';
import CommentAPI from './comment';


export type {
Entities,
Entity,
EntityTranslation,
TranslationComment,
MachineryTranslation,
OtherLocaleTranslations,
OtherLocaleTranslation,
Expand All @@ -23,6 +25,7 @@ export type {

export default {
entity: new EntityAPI(),
comment: new CommentAPI(),
filter: new FilterAPI(),
locale: new LocaleAPI(),
l10n: new L10nAPI(),
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/core/api/types.js
Expand Up @@ -14,6 +14,20 @@ export type EntityTranslation = {|
|};


/**
* Comments pertaining to a translation.
*/
export type TranslationComment = {|
+author: string,
+username: string,
+userGravatarUrlSmall: string,
+createdAt: string,
+dateIso: string,
+content: string,
+id: number,
|};


/**
* String that needs to be translated, along with its current metadata,
* and its currently accepted translations.
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/core/comments/components/AddComment.css
@@ -0,0 +1,10 @@
.comments-list .add-comment textarea {
background-color: #333941;
border: none;
border-radius: 4px;
color: #FFFFFF;
font-size: 11px;
height: 24px;
line-height: 24px;
padding: 6px;
}
76 changes: 76 additions & 0 deletions frontend/src/core/comments/components/AddComment.js
@@ -0,0 +1,76 @@
/* @flow */

import * as React from 'react';
import { Localized } from '@fluent/react';

import './AddComment.css';

import { UserAvatar } from 'core/user'

type Props = {|
user: string,
username: string,
imageURL: string,
translationId: number,
addComment: (string, number) => void,
|};


export default function AddComments(props: Props) {
const {
user,
username,
imageURL,
translationId,
addComment,
} = props;

let commentInput: any = React.useRef();

if (!user) {
return null;
}

const onEnterSubmit = (event: SyntheticKeyboardEvent<HTMLTextAreaElement>) => {
if (event.keyCode === 13 && event.shiftKey === false) {
event.preventDefault();
submitComment(event);
}
}

const submitComment = (event: SyntheticKeyboardEvent<>) => {
event.preventDefault();
const comment = commentInput.current.value;

if (!comment) {
return null;
}

addComment(comment, translationId);
commentInput.current.value = '';
};

return <div className='comment add-comment'>
<UserAvatar
user={ user }
username={ username }
title=''
imageUrl={ imageURL }
/>
<form className='container'>
<Localized
id='comments-AddComment--input'
attrs={{ placeholder: true }}
>
<textarea
id='comment-input'
name='comment'
dir='auto'
placeholder={ `Write a comment…` }
ref={ commentInput }
onKeyDown={ onEnterSubmit }
/>
</Localized>
</form>
</div>
}
29 changes: 29 additions & 0 deletions frontend/src/core/comments/components/AddComment.test.js
@@ -0,0 +1,29 @@
import React from 'react';
import { shallow } from 'enzyme';
import sinon from 'sinon';

import AddComment from './AddComment';


const DEFAULT_USER = {
user: 'RSwanson',
username: 'Ron_Swanson',
imageURL: '',
}

describe('<AddComment>', () => {
it('calls submitComment function', () => {
const submitCommentFn = sinon.spy();
const wrapper = shallow(<AddComment
{ ...DEFAULT_USER }
submitComment={ submitCommentFn }
/>);

const event = {
preventDefault: sinon.spy(),
};

wrapper.find('form').simulate('submit', event);
expect(submitCommentFn.calledOnce).toBeTruthy;
});
});
30 changes: 30 additions & 0 deletions frontend/src/core/comments/components/Comment.css
@@ -0,0 +1,30 @@
.comments-list ul .comment {
padding-bottom: 10px;
}

.comments-list .comment a {
color: #7BC876;
font-weight: 400;
}

.comments-list .comment .content {
background-color: #4d5967;
border: solid 1px #4d5967;
border-radius: 4px;
display: flex;
font-size: 11px;
padding: 6px;
}

.comments-list .comment .content p {
color: #CCCCCC;
margin-left: 4px;
}

.comments-list .comment .info {
color: #AAAAAA;
font-size: 11px;
font-weight: 300;
margin-top: -2px;
padding-left: 8px;
}
54 changes: 54 additions & 0 deletions frontend/src/core/comments/components/Comment.js
@@ -0,0 +1,54 @@
/* @flow */

import * as React from 'react';
import ReactTimeAgo from 'react-time-ago';

import './Comment.css';

import { UserAvatar } from 'core/user'

import type { TranslationComment } from 'core/api';


type Props = {|
comment: TranslationComment,
|};


export default function Comment(props: Props) {
const { comment } = props;

if (!comment) {
return null;
}

return <li className='comment'>
<UserAvatar
user={ comment.author }
username={ comment.username }
imageUrl={ comment.userGravatarUrlSmall }
/>
<div className='container'>
<div className='content' dir='auto'>
<a
href={ `/contributors/${comment.username}` }
target='_blank'
rel='noopener noreferrer'
onClick={ (e: SyntheticMouseEvent<>) => e.stopPropagation() }
>
{ comment.author }
</a>
<p>
{ comment.content }
</p>
</div>
<div className='info'>
<ReactTimeAgo
dir='ltr'
date={ new Date(comment.dateIso) }
title={ `${comment.createdAt} UTC` }
/>
</div>
</div>
</li>
}

0 comments on commit 597c496

Please sign in to comment.