mirror of
https://github.com/wassname/talk.git
synced 2026-08-13 12:40:11 +08:00
Plugins renaming
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import {forEachError} from 'coral-framework/utils';
|
||||
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import {connect} from 'react-redux';
|
||||
import {CommentForm} from './CommentForm';
|
||||
|
||||
export const name = 'talk-plugin-commentbox';
|
||||
|
||||
// Given a newly posted comment's status, show a notification to the user
|
||||
// if needed
|
||||
export const notifyForNewCommentStatus = (addNotification, status) => {
|
||||
if (status === 'REJECTED') {
|
||||
addNotification('error', t('comment_box.comment_post_banned_word'));
|
||||
} else if (status === 'PREMOD') {
|
||||
addNotification('success', t('comment_box.comment_post_notif_premod'));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Container for posting a new Comment
|
||||
*/
|
||||
class CommentBox extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
username: '',
|
||||
body: '',
|
||||
loadingState: '',
|
||||
|
||||
hooks: {
|
||||
preSubmit: [],
|
||||
postSubmit: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
const {
|
||||
commentPostedHandler,
|
||||
postComment,
|
||||
assetId,
|
||||
parentId,
|
||||
addNotification,
|
||||
currentUser,
|
||||
} = this.props;
|
||||
|
||||
if (!can(currentUser, 'INTERACT_WITH_COMMUNITY')) {
|
||||
addNotification('error', t('error.NOT_AUTHORIZED'));
|
||||
return;
|
||||
}
|
||||
|
||||
let comment = {
|
||||
asset_id: assetId,
|
||||
parent_id: parentId,
|
||||
body: this.state.body,
|
||||
...this.props.commentBox
|
||||
};
|
||||
|
||||
// Execute preSubmit Hooks
|
||||
this.state.hooks.preSubmit.forEach((hook) => hook());
|
||||
this.setState({loadingState: 'loading'});
|
||||
|
||||
postComment(comment, 'comments')
|
||||
.then(({data}) => {
|
||||
this.setState({loadingState: 'success', body: ''});
|
||||
const postedComment = data.createComment.comment;
|
||||
|
||||
// Execute postSubmit Hooks
|
||||
this.state.hooks.postSubmit.forEach((hook) => hook(data));
|
||||
|
||||
notifyForNewCommentStatus(addNotification, postedComment.status);
|
||||
|
||||
if (commentPostedHandler) {
|
||||
commentPostedHandler();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.setState({loadingState: 'error'});
|
||||
forEachError(err, ({msg}) => addNotification('error', msg));
|
||||
});
|
||||
}
|
||||
|
||||
handleBodyChange = (body) => {
|
||||
this.setState({body});
|
||||
}
|
||||
|
||||
registerHook = (hookType = '', hook = () => {}) => {
|
||||
if (typeof hook !== 'function') {
|
||||
return console.warn(`Hooks must be functions. Please check your ${hookType} hooks`);
|
||||
} else if (typeof hookType === 'string') {
|
||||
this.setState((state) => ({
|
||||
hooks: {
|
||||
...state.hooks,
|
||||
[hookType]: [
|
||||
...state.hooks[hookType],
|
||||
hook
|
||||
]
|
||||
}
|
||||
}));
|
||||
|
||||
return {
|
||||
hookType,
|
||||
hook
|
||||
};
|
||||
|
||||
} else {
|
||||
return console.warn('hookTypes must be a string. Please check your hooks');
|
||||
}
|
||||
}
|
||||
|
||||
unregisterHook = (hookData) => {
|
||||
const {hookType, hook} = hookData;
|
||||
|
||||
this.setState((state) => {
|
||||
let newHooks = state.hooks[newHooks];
|
||||
const idx = state.hooks[hookType].indexOf(hook);
|
||||
|
||||
if (idx !== -1) {
|
||||
newHooks = [
|
||||
...state.hooks[hookType].slice(0, idx),
|
||||
...state.hooks[hookType].slice(idx + 1)
|
||||
];
|
||||
}
|
||||
|
||||
return {
|
||||
hooks: {
|
||||
...state.hooks,
|
||||
[hookType]: newHooks
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const {isReply, maxCharCount} = this.props;
|
||||
let {onCancel} = this.props;
|
||||
|
||||
if (isReply && typeof onCancel !== 'function') {
|
||||
console.warn('the CommentBox component should have a onCancel callback defined if it lives in a Reply');
|
||||
onCancel = () => {};
|
||||
}
|
||||
|
||||
return <div>
|
||||
<CommentForm
|
||||
defaultValue={this.props.defaultValue}
|
||||
bodyInputId={isReply ? 'replyText' : 'commentText'}
|
||||
bodyLabel={isReply ? t('comment_box.reply') : t('comment.comment')}
|
||||
maxCharCount={maxCharCount}
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
bodyPlaceholder={t('comment.comment')}
|
||||
bodyInputId={isReply ? 'replyText' : 'commentText'}
|
||||
body={this.state.body}
|
||||
buttonContainerStart={<Slot
|
||||
fill="commentInputDetailArea"
|
||||
registerHook={this.registerHook}
|
||||
unregisterHook={this.unregisterHook}
|
||||
isReply={isReply}
|
||||
inline
|
||||
/>}
|
||||
onBodyChange={this.handleBodyChange}
|
||||
loadingState={this.state.loadingState}
|
||||
onCancel={onCancel}
|
||||
onSubmit={this.handleSubmit}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
CommentBox.propTypes = {
|
||||
|
||||
// Initial value for underlying comment body textarea
|
||||
defaultValue: PropTypes.string,
|
||||
charCountEnable: PropTypes.bool.isRequired,
|
||||
maxCharCount: PropTypes.number,
|
||||
commentPostedHandler: PropTypes.func,
|
||||
postComment: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
assetId: PropTypes.string.isRequired,
|
||||
parentId: PropTypes.string,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
isReply: PropTypes.bool.isRequired,
|
||||
canPost: PropTypes.bool,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({commentBox}) => ({commentBox});
|
||||
|
||||
export default connect(mapStateToProps, null)(CommentBox);
|
||||
@@ -0,0 +1,142 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Button} from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
|
||||
import {name} from './CommentBox';
|
||||
import styles from './styles.css';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
/**
|
||||
* Common UI for Creating or Editing a Comment
|
||||
*/
|
||||
export class CommentForm extends React.Component {
|
||||
static propTypes = {
|
||||
|
||||
charCountEnable: PropTypes.bool.isRequired,
|
||||
maxCharCount: PropTypes.number,
|
||||
|
||||
// DOM ID for form input that edits comment body
|
||||
bodyInputId: PropTypes.string,
|
||||
|
||||
// screen reader label for input that edits comment body
|
||||
bodyLabel: PropTypes.string,
|
||||
|
||||
// Placeholder for input that edits comment body
|
||||
bodyPlaceholder: PropTypes.string,
|
||||
|
||||
// render at start of button container (useful for extra buttons)
|
||||
buttonContainerStart: PropTypes.node,
|
||||
|
||||
// render inside submit button
|
||||
submitText: PropTypes.node,
|
||||
|
||||
// cStyle for enabled submit <coral-ui/Button>
|
||||
submitButtonCStyle: PropTypes.string,
|
||||
|
||||
// return whether the submit button should be enabled for the provided
|
||||
// comment ({ body }) (for reasons other than charCount)
|
||||
submitEnabled: PropTypes.func,
|
||||
|
||||
// className to add to buttons
|
||||
submitButtonClassName: PropTypes.string,
|
||||
cancelButtonClassName: PropTypes.string,
|
||||
|
||||
body: PropTypes.string.isRequired,
|
||||
onBodyChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
state: PropTypes.string,
|
||||
loadingState: PropTypes.oneOf(['', 'loading', 'success', 'error']),
|
||||
}
|
||||
static get defaultProps() {
|
||||
return {
|
||||
bodyLabel: t('comment_box.comment'),
|
||||
bodyPlaceholder: t('comment_box.comment'),
|
||||
submitText: t('comment_box.post'),
|
||||
submitButtonCStyle: 'darkGrey',
|
||||
submitEnabled: () => true,
|
||||
};
|
||||
}
|
||||
|
||||
onBodyChange = (e) => {
|
||||
this.props.onBodyChange(e.target.value);
|
||||
}
|
||||
|
||||
onClickSubmit = () => {
|
||||
this.props.onSubmit();
|
||||
}
|
||||
|
||||
getButtonClassName = () => {
|
||||
switch (this.props.loadingState) {
|
||||
case 'loading':
|
||||
return cn(`${name}-button-loading`, styles.buttonLoading);
|
||||
case 'success':
|
||||
return cn(`${name}-button-success`, styles.buttonSuccess);
|
||||
case 'error':
|
||||
return cn(`${name}-button-error`, styles.buttonError);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {maxCharCount, submitEnabled, cancelButtonClassName, submitButtonClassName, charCountEnable, body, loadingState} = this.props;
|
||||
|
||||
const length = body.length;
|
||||
const isRespectingMaxCount = (length) => charCountEnable && maxCharCount && length > maxCharCount;
|
||||
const disableSubmitButton = !length || isRespectingMaxCount(length) || !submitEnabled({body}) || loadingState === 'loading';
|
||||
const disableCancelButton = loadingState === 'loading';
|
||||
const disableTextArea = loadingState === 'loading';
|
||||
|
||||
return <div>
|
||||
<div className={`${name}-container`}>
|
||||
<label
|
||||
htmlFor={this.props.bodyInputId}
|
||||
className="screen-reader-text"
|
||||
aria-hidden={true}>
|
||||
{this.props.bodyLabel}
|
||||
</label>
|
||||
<textarea
|
||||
className={`${name}-textarea`}
|
||||
value={body}
|
||||
placeholder={this.props.bodyPlaceholder}
|
||||
id={this.props.bodyInputId}
|
||||
onChange={this.onBodyChange}
|
||||
rows={3}
|
||||
disabled={disableTextArea}
|
||||
/>
|
||||
<Slot fill='commentInputArea' />
|
||||
</div>
|
||||
{
|
||||
this.props.charCountEnable &&
|
||||
<div className={`${name}-char-count ${length > maxCharCount ? `${name}-char-max` : ''}`}>
|
||||
{maxCharCount && `${maxCharCount - length} ${t('comment_box.characters_remaining')}`}
|
||||
</div>
|
||||
}
|
||||
<div className={`${name}-button-container`}>
|
||||
{ this.props.buttonContainerStart }
|
||||
{
|
||||
typeof this.props.onCancel === 'function' && (
|
||||
<Button
|
||||
cStyle='darkGrey'
|
||||
className={cn(`${name}-cancel-button`, cancelButtonClassName)}
|
||||
onClick={this.props.onCancel}
|
||||
disabled={disableCancelButton}
|
||||
>
|
||||
{t('comment_box.cancel')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
cStyle={disableSubmitButton ? 'lightGrey' : this.props.submitButtonCStyle}
|
||||
className={cn(`${name}-button`, submitButtonClassName, this.getButtonClassName())}
|
||||
onClick={this.onClickSubmit}
|
||||
disabled={disableSubmitButton ? 'disabled' : ''}>
|
||||
{this.props.submitText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {expect} from 'chai';
|
||||
import CommentBox from '../CommentBox';
|
||||
|
||||
describe('CommentBox', () => {
|
||||
let comment;
|
||||
let render;
|
||||
beforeEach(() => {
|
||||
comment = {};
|
||||
const postItem = (item) => {
|
||||
comment.posted = item;
|
||||
return Promise.resolve(4);
|
||||
};
|
||||
render = shallow(<CommentBox
|
||||
postItem={postItem}
|
||||
updateItem={(e) => comment.text = e.target.value}
|
||||
item_id={'1'}
|
||||
comments={['1', '2', '3']}/>);
|
||||
});
|
||||
|
||||
it('should render the CommentBox appropriately', () => {
|
||||
expect(render.contains('<div class="CommentBox"')).to.be.true;
|
||||
expect(render.contains('<button class="postCommentButton"')).to.be.true;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
export const addTag = (tag) => ({
|
||||
type: 'ADD_TAG',
|
||||
tag
|
||||
});
|
||||
|
||||
export const removeTag = (idx) => ({
|
||||
type: 'REMOVE_TAG',
|
||||
idx
|
||||
});
|
||||
|
||||
export const clearTags = () => ({
|
||||
type: 'CLEAR_TAGS',
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export const ADD_TAG = 'ADD_TAG';
|
||||
export const REMOVE_TAG = 'REMOVE_TAG';
|
||||
export const CLEAR_TAGS = 'CLEAR_TAGS';
|
||||
@@ -0,0 +1,5 @@
|
||||
import reducer from './reducer';
|
||||
|
||||
export default {
|
||||
reducer
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import {ADD_TAG, REMOVE_TAG, CLEAR_TAGS} from './constants';
|
||||
|
||||
const initialState = {
|
||||
tags: []
|
||||
};
|
||||
|
||||
export default function commentBox (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case ADD_TAG :
|
||||
return {
|
||||
...state,
|
||||
tags: [...state.tags, action.tag]
|
||||
};
|
||||
case REMOVE_TAG :
|
||||
return {
|
||||
...state,
|
||||
tags: [
|
||||
...state.tags.slice(0, action.idx),
|
||||
...state.tags.slice(action.idx + 1)
|
||||
]
|
||||
};
|
||||
case CLEAR_TAGS :
|
||||
return initialState;
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Example loading state animations on the button.
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
@-webkit-keyframes sk-scaleout {
|
||||
0% { -webkit-transform: scale(0) }
|
||||
100% {
|
||||
-webkit-transform: scale(1.0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sk-scaleout {
|
||||
0% {
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0);
|
||||
} 100% {
|
||||
-webkit-transform: scale(1.0);
|
||||
transform: scale(1.0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes comeAndGo {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1.0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonLoading, .buttonLoading:disabled, .buttonLoading:hover {
|
||||
transition: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.buttonSuccess, .buttonSuccess:disabled, .buttonSuccess:hover {
|
||||
}
|
||||
|
||||
.buttonSuccess::before {
|
||||
content: '✓';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 3px 0px;
|
||||
animation: comeAndGo 2s forwards;
|
||||
background: rgb(51, 204, 51);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.buttonLoading::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
background-color: #333;
|
||||
border-radius: 100%;
|
||||
-webkit-animation: sk-scaleout 1.0s infinite ease-in-out;
|
||||
animation: sk-scaleout 1.0s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.buttonError, .buttonError:disabled, .buttonError:hover {
|
||||
}
|
||||
|
||||
.buttonError::before {
|
||||
content: '×';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 3px 0px;
|
||||
animation: comeAndGo 2s forwards;
|
||||
background: rgb(250, 100, 100);
|
||||
color: white;
|
||||
}
|
||||
|
||||
*/
|
||||
Reference in New Issue
Block a user