mirror of
https://github.com/wassname/talk.git
synced 2026-08-09 12:30:42 +08:00
Refactor draft area api step 1
This commit is contained in:
@@ -16,7 +16,7 @@ import mapValues from 'lodash/mapValues';
|
||||
import get from 'lodash/get';
|
||||
|
||||
import LoadMore from './LoadMore';
|
||||
import { getEditableUntilDate } from './util';
|
||||
import { getEditableUntilDate } from '../util';
|
||||
import { findCommentWithId } from '../../../graphql/utils';
|
||||
import CommentContent from 'coral-framework/components/CommentContent';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
|
||||
@@ -37,15 +37,15 @@ class CommentForm extends React.Component {
|
||||
submitButtonCStyle: PropTypes.string,
|
||||
|
||||
// return whether the submit button should be enabled for the provided
|
||||
// comment ({ body }) (for reasons other than charCount)
|
||||
// input (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,
|
||||
input: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
state: PropTypes.string,
|
||||
@@ -53,6 +53,7 @@ class CommentForm extends React.Component {
|
||||
registerHook: PropTypes.func,
|
||||
unregisterHook: PropTypes.func,
|
||||
isReply: PropTypes.bool,
|
||||
isEdit: PropTypes.bool,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object,
|
||||
};
|
||||
@@ -90,20 +91,20 @@ class CommentForm extends React.Component {
|
||||
cancelButtonClassName,
|
||||
submitButtonClassName,
|
||||
charCountEnable,
|
||||
body,
|
||||
input,
|
||||
loadingState,
|
||||
comment,
|
||||
root,
|
||||
} = this.props;
|
||||
|
||||
const length = body.length;
|
||||
const length = input.body.length;
|
||||
const isRespectingMaxCount = length =>
|
||||
charCountEnable && maxCharCount && length > maxCharCount;
|
||||
const disableSubmitButton =
|
||||
!length ||
|
||||
body.trim().length === 0 ||
|
||||
input.body.trim().length === 0 ||
|
||||
isRespectingMaxCount(length) ||
|
||||
!submitEnabled({ body }) ||
|
||||
!submitEnabled(input) ||
|
||||
loadingState === 'loading';
|
||||
const disableCancelButton = loadingState === 'loading';
|
||||
const disableTextArea = loadingState === 'loading';
|
||||
@@ -115,15 +116,16 @@ class CommentForm extends React.Component {
|
||||
comment={comment}
|
||||
id={this.props.bodyInputId}
|
||||
label={this.props.bodyLabel}
|
||||
value={body}
|
||||
input={input}
|
||||
placeholder={this.props.bodyPlaceholder}
|
||||
onChange={this.props.onBodyChange}
|
||||
onInputChange={this.props.onInputChange}
|
||||
disabled={disableTextArea}
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
maxCharCount={this.props.maxCharCount}
|
||||
registerHook={this.props.registerHook}
|
||||
unregisterHook={this.props.unregisterHook}
|
||||
isReply={this.props.isReply}
|
||||
isEdit={this.props.isEdit}
|
||||
/>
|
||||
<div className={cn(styles.buttonContainer, `${name}-button-container`)}>
|
||||
{this.props.buttonContainerStart}
|
||||
|
||||
@@ -13,17 +13,17 @@ import styles from './DraftArea.css';
|
||||
*/
|
||||
export default class DraftArea extends React.Component {
|
||||
renderCharCount() {
|
||||
const { value, maxCharCount } = this.props;
|
||||
const { input, maxCharCount } = this.props;
|
||||
|
||||
const className = cn(
|
||||
styles.charCount,
|
||||
'talk-plugin-commentbox-char-count',
|
||||
{
|
||||
[`${styles.charMax} talk-plugin-commentbox-char-max`]:
|
||||
value.length > maxCharCount,
|
||||
input.body.length > maxCharCount,
|
||||
}
|
||||
);
|
||||
const remaining = maxCharCount - value.length;
|
||||
const remaining = maxCharCount - input.body.length;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
@@ -34,7 +34,7 @@ export default class DraftArea extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
value,
|
||||
input,
|
||||
placeholder,
|
||||
id,
|
||||
disabled,
|
||||
@@ -42,8 +42,9 @@ export default class DraftArea extends React.Component {
|
||||
label,
|
||||
charCountEnable,
|
||||
maxCharCount,
|
||||
onChange,
|
||||
onInputChange,
|
||||
isReply,
|
||||
isEdit,
|
||||
registerHook,
|
||||
unregisterHook,
|
||||
root,
|
||||
@@ -51,7 +52,7 @@ export default class DraftArea extends React.Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id={id}>
|
||||
<div
|
||||
className={cn(styles.container, 'talk-plugin-commentbox-container')}
|
||||
>
|
||||
@@ -67,13 +68,14 @@ export default class DraftArea extends React.Component {
|
||||
comment,
|
||||
registerHook,
|
||||
unregisterHook,
|
||||
value,
|
||||
input,
|
||||
placeholder,
|
||||
id,
|
||||
onChange,
|
||||
onInputChange,
|
||||
rows,
|
||||
disabled,
|
||||
isReply,
|
||||
isEdit,
|
||||
}}
|
||||
/>
|
||||
<Slot fill="commentInputArea" />
|
||||
@@ -92,10 +94,10 @@ DraftArea.propTypes = {
|
||||
charCountEnable: PropTypes.bool,
|
||||
maxCharCount: PropTypes.number,
|
||||
id: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
input: PropTypes.object,
|
||||
placeholder: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onInputChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
rows: PropTypes.number,
|
||||
root: PropTypes.object.isRequired,
|
||||
@@ -103,4 +105,5 @@ DraftArea.propTypes = {
|
||||
registerHook: PropTypes.func,
|
||||
unregisterHook: PropTypes.func,
|
||||
isReply: PropTypes.bool,
|
||||
isEdit: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -4,19 +4,19 @@ import cn from 'classnames';
|
||||
import styles from './DraftAreaContent.css';
|
||||
|
||||
const DraftAreaContent = ({
|
||||
value,
|
||||
input,
|
||||
placeholder,
|
||||
id,
|
||||
onChange,
|
||||
onInputChange,
|
||||
rows,
|
||||
disabled,
|
||||
}) => (
|
||||
<textarea
|
||||
className={cn(styles.content, 'talk-plugin-commentbox-textarea')}
|
||||
value={value}
|
||||
value={input.body}
|
||||
placeholder={placeholder}
|
||||
id={id}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
onChange={e => onInputChange({ body: e.target.value })}
|
||||
rows={rows}
|
||||
disabled={disabled}
|
||||
/>
|
||||
@@ -28,9 +28,9 @@ DraftAreaContent.defaultProps = {
|
||||
|
||||
DraftAreaContent.propTypes = {
|
||||
id: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
input: PropTypes.object,
|
||||
placeholder: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onInputChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
rows: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { notifyForNewCommentStatus } from '../helpers';
|
||||
import CommentForm from '../containers/CommentForm';
|
||||
import styles from './Comment.css';
|
||||
import { CountdownSeconds } from './CountdownSeconds';
|
||||
import { getEditableUntilDate } from './util';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
|
||||
import { Icon } from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -15,187 +12,83 @@ import t from 'coral-framework/services/i18n';
|
||||
* Renders a Comment's body in such a way that the end-user can edit it and save changes
|
||||
*/
|
||||
class EditableCommentContent extends React.Component {
|
||||
static propTypes = {
|
||||
// show notification to the user (e.g. for errors)
|
||||
notify: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
// comment that is being edited
|
||||
comment: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
body: PropTypes.string,
|
||||
editing: PropTypes.shape({
|
||||
edited: PropTypes.bool,
|
||||
|
||||
// ISO8601
|
||||
editableUntil: PropTypes.string,
|
||||
}),
|
||||
}).isRequired,
|
||||
|
||||
// logged in user
|
||||
currentUser: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
}),
|
||||
charCountEnable: PropTypes.bool,
|
||||
maxCharCount: PropTypes.number,
|
||||
|
||||
// edit a comment, passed {{ body }}
|
||||
editComment: PropTypes.func,
|
||||
|
||||
// called when editing should be stopped
|
||||
stopEditing: PropTypes.func,
|
||||
};
|
||||
|
||||
unmounted = false;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.editWindowExpiryTimeout = null;
|
||||
this.state = {
|
||||
body: props.comment.body,
|
||||
loadingState: '',
|
||||
// data: {@object} contains data that might be useful for plugins, metadata, etc
|
||||
data: {},
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
const editableUntil = getEditableUntilDate(this.props.comment);
|
||||
const now = new Date();
|
||||
const editWindowRemainingMs = editableUntil && editableUntil - now;
|
||||
if (editWindowRemainingMs > 0) {
|
||||
this.editWindowExpiryTimeout = setTimeout(() => {
|
||||
this.forceUpdate();
|
||||
}, editWindowRemainingMs);
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
if (this.editWindowExpiryTimeout) {
|
||||
this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
handleBodyChange = (body, data) => {
|
||||
this.setState(state => ({
|
||||
body,
|
||||
data: {
|
||||
...state.data,
|
||||
...data,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
handleSubmit = async () => {
|
||||
if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
|
||||
this.props.notify('error', t('error.NOT_AUTHORIZED'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ loadingState: 'loading' });
|
||||
|
||||
const { editComment, stopEditing } = this.props;
|
||||
if (typeof editComment !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
let input = {
|
||||
body: this.state.body,
|
||||
...this.state.data,
|
||||
};
|
||||
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await editComment(input);
|
||||
if (!this.unmounted) {
|
||||
this.setState({ loadingState: 'success' });
|
||||
}
|
||||
const status = response.data.editComment.comment.status;
|
||||
notifyForNewCommentStatus(this.props.notify, status);
|
||||
if (typeof stopEditing === 'function') {
|
||||
stopEditing();
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState({ loadingState: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
getEditableUntil = (props = this.props) => {
|
||||
return getEditableUntilDate(props.comment);
|
||||
};
|
||||
|
||||
isEditWindowExpired = (props = this.props) => {
|
||||
return this.getEditableUntil(props) - new Date() < 0;
|
||||
};
|
||||
|
||||
isSubmitEnabled = comment => {
|
||||
// should be disabled if user hasn't actually changed their
|
||||
// original comment
|
||||
renderButtonContainerStart() {
|
||||
return (
|
||||
comment.body !== this.props.comment.body && !this.isEditWindowExpired()
|
||||
<div className={styles.buttonContainerLeft}>
|
||||
<span className={styles.editWindowRemaining}>
|
||||
{this.props.editWindowExpired ? (
|
||||
<span>
|
||||
{t('edit_comment.edit_window_expired')}
|
||||
<span>
|
||||
<a className={styles.link} onClick={this.props.onCancel}>
|
||||
{t('edit_comment.edit_window_expired_close')}
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<Icon name="timer" className={styles.timerIcon} />{' '}
|
||||
{t('edit_comment.edit_window_timer_prefix')}
|
||||
<CountdownSeconds
|
||||
until={this.props.editableUntil}
|
||||
classNameForMsRemaining={remainingMs =>
|
||||
remainingMs <= 10 * 1000 ? styles.editWindowAlmostOver : ''
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const id = `edit-draft_${this.props.comment.id}`;
|
||||
return (
|
||||
<div className={styles.editCommentForm}>
|
||||
<CommentForm
|
||||
isEdit
|
||||
root={this.props.root}
|
||||
comment={this.props.comment}
|
||||
defaultValue={this.props.comment.body}
|
||||
bodyInputId={id}
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
maxCharCount={this.props.maxCharCount}
|
||||
submitEnabled={this.isSubmitEnabled}
|
||||
body={this.state.body}
|
||||
onBodyChange={this.handleBodyChange}
|
||||
onSubmit={this.handleSubmit}
|
||||
submitEnabled={this.props.submitEnabled}
|
||||
input={this.props.input}
|
||||
onInputChange={this.props.onInputChange}
|
||||
onSubmit={this.props.onSubmit}
|
||||
onCancel={this.props.onCancel}
|
||||
loadingState={this.props.loadingState}
|
||||
registerHook={this.props.registerHook}
|
||||
unregisterHook={this.props.unregisterHook}
|
||||
buttonContainerStart={this.renderButtonContainerStart()}
|
||||
submitButtonClassName={styles.button}
|
||||
cancelButtonClassName={styles.button}
|
||||
bodyLabel={t('edit_comment.body_input_label')}
|
||||
bodyPlaceholder=""
|
||||
submitText={<span>{t('edit_comment.save_button')}</span>}
|
||||
submitButtonCStyle="green"
|
||||
onCancel={this.props.stopEditing}
|
||||
submitButtonClassName={styles.button}
|
||||
cancelButtonClassName={styles.button}
|
||||
loadingState={this.state.loadingState}
|
||||
buttonContainerStart={
|
||||
<div className={styles.buttonContainerLeft}>
|
||||
<span className={styles.editWindowRemaining}>
|
||||
{this.isEditWindowExpired() ? (
|
||||
<span>
|
||||
{t('edit_comment.edit_window_expired')}
|
||||
{typeof this.props.stopEditing === 'function' ? (
|
||||
<span>
|
||||
<a
|
||||
className={styles.link}
|
||||
onClick={this.props.stopEditing}
|
||||
>
|
||||
{t('edit_comment.edit_window_expired_close')}
|
||||
</a>
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<Icon name="timer" className={styles.timerIcon} />{' '}
|
||||
{t('edit_comment.edit_window_timer_prefix')}
|
||||
<CountdownSeconds
|
||||
until={this.getEditableUntil()}
|
||||
classNameForMsRemaining={remainingMs =>
|
||||
remainingMs <= 10 * 1000
|
||||
? styles.editWindowAlmostOver
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
bodyInputId={id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditableCommentContent.propTypes = {
|
||||
charCountEnable: PropTypes.bool,
|
||||
submitEnabled: PropTypes.func,
|
||||
maxCharCount: PropTypes.number,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object.isRequired,
|
||||
input: PropTypes.object.isRequired,
|
||||
registerHook: PropTypes.func.isRequired,
|
||||
unregisterHook: PropTypes.func.isRequired,
|
||||
onInputChange: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
loadingState: PropTypes.string,
|
||||
editWindowExpired: PropTypes.bool,
|
||||
editableUntil: PropTypes.object,
|
||||
};
|
||||
|
||||
export default EditableCommentContent;
|
||||
|
||||
@@ -7,6 +7,8 @@ import Slot from 'coral-framework/components/Slot';
|
||||
import { connect } from 'react-redux';
|
||||
import CommentForm from '../containers/CommentForm';
|
||||
import { notifyForNewCommentStatus } from '../helpers';
|
||||
import withHooks from '../hocs/withHooks';
|
||||
import { compose } from 'recompose';
|
||||
|
||||
// TODO: (kiwi) Need to adapt CSS classes post refactor to match the rest.
|
||||
export const name = 'talk-plugin-commentbox';
|
||||
@@ -19,13 +21,9 @@ class CommentBox extends React.Component {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
body: '',
|
||||
loadingState: '',
|
||||
// data: {@object} contains data that might be useful for plugins
|
||||
data: {},
|
||||
hooks: {
|
||||
preSubmit: [],
|
||||
postSubmit: [],
|
||||
input: {
|
||||
body: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -59,13 +57,12 @@ class CommentBox extends React.Component {
|
||||
let input = {
|
||||
asset_id: assetId,
|
||||
parent_id: parentId,
|
||||
body: this.state.body,
|
||||
tags: this.props.tags,
|
||||
...this.state.data,
|
||||
...this.state.input,
|
||||
};
|
||||
|
||||
// Execute preSubmit Hooks
|
||||
this.state.hooks.preSubmit.forEach(hook => {
|
||||
this.props.forEachHook('preSubmit', hook => {
|
||||
const result = hook(input);
|
||||
if (result) {
|
||||
input = result;
|
||||
@@ -75,13 +72,13 @@ class CommentBox extends React.Component {
|
||||
|
||||
postComment(input, 'comments')
|
||||
.then(({ data }) => {
|
||||
this.setState({ loadingState: 'success', body: '' });
|
||||
this.setState({ loadingState: 'success', input: { body: '' } });
|
||||
const postedComment = data.createComment.comment;
|
||||
const actions = data.createComment.actions;
|
||||
|
||||
// Execute postSubmit Hooks
|
||||
this.state.hooks.postSubmit.forEach(hook =>
|
||||
hook(data, this.handleBodyChange)
|
||||
this.props.forEachHook('postSubmit', hook =>
|
||||
hook(data, this.handleInputChange)
|
||||
);
|
||||
|
||||
notifyForNewCommentStatus(notify, postedComment.status, actions);
|
||||
@@ -95,62 +92,29 @@ class CommentBox extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
handleBodyChange = (body, data) => {
|
||||
handleInputChange = input => {
|
||||
this.setState(state => ({
|
||||
body,
|
||||
data: {
|
||||
...state.data,
|
||||
...data,
|
||||
input: {
|
||||
...state.input,
|
||||
...input,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
renderButtonContainerStart() {
|
||||
const { isReply, registerHook, unregisterHook } = this.props;
|
||||
return (
|
||||
<Slot
|
||||
fill="commentInputDetailArea"
|
||||
passthrough={{
|
||||
registerHook: registerHook,
|
||||
unregisterHook: unregisterHook,
|
||||
isReply,
|
||||
}}
|
||||
inline
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
@@ -160,6 +124,8 @@ class CommentBox extends React.Component {
|
||||
parentId,
|
||||
comment,
|
||||
root,
|
||||
registerHook,
|
||||
unregisterHook,
|
||||
} = this.props;
|
||||
let { onCancel } = this.props;
|
||||
|
||||
@@ -186,22 +152,12 @@ class CommentBox extends React.Component {
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
bodyPlaceholder={t('comment.comment')}
|
||||
bodyInputId={id}
|
||||
body={this.state.body}
|
||||
registerHook={this.registerHook}
|
||||
unregisterHook={this.unregisterHook}
|
||||
input={this.state.input}
|
||||
registerHook={registerHook}
|
||||
unregisterHook={unregisterHook}
|
||||
isReply={isReply}
|
||||
buttonContainerStart={
|
||||
<Slot
|
||||
fill="commentInputDetailArea"
|
||||
passthrough={{
|
||||
registerHook: this.registerHook,
|
||||
unregisterHook: this.unregisterHook,
|
||||
isReply,
|
||||
}}
|
||||
inline
|
||||
/>
|
||||
}
|
||||
onBodyChange={this.handleBodyChange}
|
||||
buttonContainerStart={this.renderButtonContainerStart()}
|
||||
onInputChange={this.handleInputChange}
|
||||
loadingState={this.state.loadingState}
|
||||
onCancel={onCancel}
|
||||
onSubmit={this.handleSubmit}
|
||||
@@ -228,6 +184,9 @@ CommentBox.propTypes = {
|
||||
tags: PropTypes.array,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object,
|
||||
registerHook: PropTypes.func.isRequired,
|
||||
unregisterHook: PropTypes.func.isRequired,
|
||||
forEachHook: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
CommentBox.fragments = CommentForm.fragments;
|
||||
@@ -236,4 +195,9 @@ const mapStateToProps = state => ({
|
||||
tags: state.stream.commentBoxTags,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(CommentBox);
|
||||
const enhance = compose(
|
||||
withHooks(['preSubmit', 'postSubmit']),
|
||||
connect(mapStateToProps, null)
|
||||
);
|
||||
|
||||
export default enhance(CommentBox);
|
||||
|
||||
@@ -17,9 +17,17 @@ class DraftAreaContainer extends React.Component {
|
||||
}
|
||||
|
||||
async initValue() {
|
||||
const value = await this.context.pymSessionStorage.getItem(this.getPath());
|
||||
if (value && this.props.onChange) {
|
||||
this.props.onChange(value);
|
||||
const input = await this.context.pymSessionStorage.getItem(this.getPath());
|
||||
if (input && this.props.onInputChange) {
|
||||
let parsed = '';
|
||||
|
||||
// Older version saved a normal string, catch those and ignore them.
|
||||
try {
|
||||
parsed = JSON.parse(input);
|
||||
} catch (_e) {}
|
||||
if (typeof parsed === 'object') {
|
||||
this.props.onInputChange(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,14 +35,13 @@ class DraftAreaContainer extends React.Component {
|
||||
return `${STORAGE_PATH}_${this.props.id}`;
|
||||
};
|
||||
|
||||
onChange = (body, data) => {
|
||||
this.props.onChange && this.props.onChange(body, data);
|
||||
};
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.value !== nextProps.value) {
|
||||
if (nextProps.value) {
|
||||
this.context.pymSessionStorage.setItem(this.getPath(), nextProps.value);
|
||||
if (this.props.input !== nextProps.input) {
|
||||
if (nextProps.input) {
|
||||
this.context.pymSessionStorage.setItem(
|
||||
this.getPath(),
|
||||
JSON.stringify(nextProps.input)
|
||||
);
|
||||
} else {
|
||||
this.context.pymSessionStorage.removeItem(this.getPath());
|
||||
}
|
||||
@@ -46,10 +53,10 @@ class DraftAreaContainer extends React.Component {
|
||||
<DraftArea
|
||||
root={this.props.root}
|
||||
comment={this.props.comment}
|
||||
value={this.props.value}
|
||||
input={this.props.input}
|
||||
placeholder={this.props.placeholder}
|
||||
id={this.props.id}
|
||||
onChange={this.onChange}
|
||||
onInputChange={this.props.onInputChange}
|
||||
rows={this.props.rows}
|
||||
disabled={this.props.disabled}
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
@@ -58,6 +65,7 @@ class DraftAreaContainer extends React.Component {
|
||||
registerHook={this.props.registerHook}
|
||||
unregisterHook={this.props.unregisterHook}
|
||||
isReply={this.props.isReply}
|
||||
isEdit={this.props.isEdit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -73,15 +81,16 @@ DraftAreaContainer.propTypes = {
|
||||
charCountEnable: PropTypes.bool,
|
||||
maxCharCount: PropTypes.number,
|
||||
id: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
input: PropTypes.object.isRequired,
|
||||
placeholder: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
rows: PropTypes.number,
|
||||
label: PropTypes.string.isRequired,
|
||||
registerHook: PropTypes.func,
|
||||
unregisterHook: PropTypes.func,
|
||||
isReply: PropTypes.bool,
|
||||
isEdit: PropTypes.bool,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object,
|
||||
};
|
||||
|
||||
@@ -1,11 +1,150 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { notifyForNewCommentStatus } from '../helpers';
|
||||
import { getEditableUntilDate } from '../util';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
import EditableCommentContent from '../components/EditableCommentContent';
|
||||
import CommentForm from './CommentForm';
|
||||
import withHooks from '../hocs/withHooks';
|
||||
import { compose } from 'recompose';
|
||||
|
||||
const EditableCommentContentContainer = props => (
|
||||
<EditableCommentContent {...props} />
|
||||
);
|
||||
/**
|
||||
* Renders a Comment's body in such a way that the end-user can edit it and save changes
|
||||
*/
|
||||
class EditableCommentContentContainer extends React.Component {
|
||||
unmounted = false;
|
||||
editWindowExpiryTimeout = null;
|
||||
state = {
|
||||
loadingState: '',
|
||||
submitEnabled: false,
|
||||
input: {
|
||||
body: this.props.comment.body,
|
||||
},
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const editableUntil = getEditableUntilDate(this.props.comment);
|
||||
const now = new Date();
|
||||
const editWindowRemainingMs = editableUntil && editableUntil - now;
|
||||
if (editWindowRemainingMs > 0) {
|
||||
this.editWindowExpiryTimeout = setTimeout(() => {
|
||||
this.forceUpdate();
|
||||
}, editWindowRemainingMs);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
if (this.editWindowExpiryTimeout) {
|
||||
this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
handleInputChange = input => {
|
||||
this.setState(state => ({
|
||||
submitEnabled: true,
|
||||
input: {
|
||||
...state.input,
|
||||
...input,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
handleSubmit = async () => {
|
||||
if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
|
||||
this.props.notify('error', t('error.NOT_AUTHORIZED'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ loadingState: 'loading' });
|
||||
|
||||
const { editComment, stopEditing } = this.props;
|
||||
if (typeof editComment !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
let input = this.state.input;
|
||||
|
||||
// Execute preSubmit Hooks
|
||||
this.props.forEachHook('preSubmit', hook => {
|
||||
const result = hook(input);
|
||||
if (result) {
|
||||
input = result;
|
||||
}
|
||||
});
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await editComment(input);
|
||||
// Execute postSubmit Hooks
|
||||
this.props.forEachHook('postSubmit', hook =>
|
||||
hook(response, this.handleInputChange)
|
||||
);
|
||||
|
||||
if (!this.unmounted) {
|
||||
this.setState({ loadingState: 'success' });
|
||||
}
|
||||
const status = response.data.editComment.comment.status;
|
||||
notifyForNewCommentStatus(this.props.notify, status);
|
||||
if (typeof stopEditing === 'function') {
|
||||
stopEditing();
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState({ loadingState: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
getEditableUntil = (props = this.props) => {
|
||||
return getEditableUntilDate(props.comment);
|
||||
};
|
||||
|
||||
isEditWindowExpired = (props = this.props) => {
|
||||
return this.getEditableUntil(props) - new Date() < 0;
|
||||
};
|
||||
|
||||
isSubmitEnabled = () => this.state.submitEnabled;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditableCommentContent
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
submitEnabled={this.isSubmitEnabled}
|
||||
maxCharCount={this.props.maxCharCount}
|
||||
root={this.props.root}
|
||||
comment={this.props.comment}
|
||||
input={this.state.input}
|
||||
onInputChange={this.handleInputChange}
|
||||
onSubmit={this.handleSubmit}
|
||||
onCancel={this.props.stopEditing}
|
||||
loadingState={this.state.loadingState}
|
||||
editWindowExpired={this.isEditWindowExpired()}
|
||||
editableUntil={this.getEditableUntil()}
|
||||
registerHook={this.props.registerHook}
|
||||
unregisterHook={this.props.unregisterHook}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditableCommentContentContainer.propTypes = {
|
||||
notify: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object.isRequired,
|
||||
currentUser: PropTypes.object,
|
||||
charCountEnable: PropTypes.bool,
|
||||
maxCharCount: PropTypes.number,
|
||||
editComment: PropTypes.func,
|
||||
stopEditing: PropTypes.func,
|
||||
registerHook: PropTypes.func.isRequired,
|
||||
unregisterHook: PropTypes.func.isRequired,
|
||||
forEachHook: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
EditableCommentContentContainer.fragments = CommentForm.fragments;
|
||||
|
||||
export default EditableCommentContentContainer;
|
||||
const enhance = compose(withHooks(['preSubmit', 'postSubmit']));
|
||||
|
||||
export default enhance(EditableCommentContentContainer);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
|
||||
/**
|
||||
* WithHooks provides a property `hooks` to the wrapped component.
|
||||
*/
|
||||
export default hooks =>
|
||||
hoistStatics(WrappedComponent => {
|
||||
class WithHooks extends React.Component {
|
||||
hooks = hooks.reduce((map, key) => {
|
||||
map[key] = [];
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
registerHook = (hookType = '', hook) => {
|
||||
if (typeof hook !== 'function') {
|
||||
return console.warn(
|
||||
`Hooks must be functions. Please check your ${hookType} hooks`
|
||||
);
|
||||
}
|
||||
if (!hooks.includes(hookType)) {
|
||||
throw new Error(`Unknown hookType ${hookType}`);
|
||||
}
|
||||
|
||||
this.hooks[hookType].push(hook);
|
||||
return {
|
||||
hookType,
|
||||
hook,
|
||||
};
|
||||
};
|
||||
|
||||
unregisterHook = hookData => {
|
||||
const { hookType, hook } = hookData;
|
||||
const idx = this.hooks[hookType].indexOf(hook);
|
||||
if (idx !== -1) {
|
||||
this.hooks[hookType].splice(idx, 1);
|
||||
}
|
||||
};
|
||||
|
||||
forEachHook = (hookType, callback) => {
|
||||
this.hooks[hookType].forEach(callback);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...this.props}
|
||||
registerHook={this.registerHook}
|
||||
unregisterHook={this.unregisterHook}
|
||||
forEachHook={this.forEachHook}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return WithHooks;
|
||||
});
|
||||
@@ -12,42 +12,38 @@ import bowser from 'bowser';
|
||||
class Editor extends React.Component {
|
||||
ref = null;
|
||||
handleRef = ref => (this.ref = ref);
|
||||
state = {
|
||||
html:
|
||||
!this.props.isReply && this.props.comment
|
||||
? this.props.comment.richTextBody || this.props.comment.body || ''
|
||||
: '',
|
||||
};
|
||||
|
||||
handleChange = evt => {
|
||||
const html = evt.target.value;
|
||||
this.setState({ html });
|
||||
this.props.onChange(this.ref.htmlEl.innerText, {
|
||||
richTextBody: htmlNormalizer(html),
|
||||
this.props.onInputChange({
|
||||
body: this.ref.htmlEl.innerText,
|
||||
richTextBody: evt.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.registerHook) {
|
||||
this.clearInputHook = this.props.registerHook(
|
||||
'postSubmit',
|
||||
(res, handleBodyChange) => {
|
||||
this.setState({ html: '' });
|
||||
handleBodyChange('', { richTextBody: '' });
|
||||
}
|
||||
);
|
||||
getHTML(props = this.props) {
|
||||
if (props.input.richTextBody) {
|
||||
return props.input.richTextBody;
|
||||
}
|
||||
return (
|
||||
(props.isEdit && (props.comment.richTextBody || props.comment.body)) || ''
|
||||
);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (this.props.value !== nextProps.value) {
|
||||
return false;
|
||||
componentDidMount() {
|
||||
if (this.props.registerHook) {
|
||||
this.normalizeHook = this.props.registerHook('preSubmit', input => {
|
||||
if (input.richTextBody) {
|
||||
return {
|
||||
...input,
|
||||
richTextBody: htmlNormalizer(input.richTextBody),
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.unregisterHook(this.clearInputHook);
|
||||
this.props.unregisterHook(this.normalizeHook);
|
||||
}
|
||||
|
||||
getCurrentTagName() {
|
||||
@@ -90,7 +86,6 @@ class Editor extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { id } = this.props;
|
||||
return (
|
||||
<div className={cn(styles.root, `${PLUGIN_NAME}-container`)}>
|
||||
<Toolbar>
|
||||
@@ -109,9 +104,8 @@ class Editor extends React.Component {
|
||||
<ContentEditable
|
||||
onKeyPress={this.outdentOnEnter}
|
||||
className={styles.contentEditable}
|
||||
id={id}
|
||||
ref={this.handleRef}
|
||||
html={this.state.html}
|
||||
html={this.getHTML()}
|
||||
disabled={false}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
@@ -121,17 +115,16 @@ class Editor extends React.Component {
|
||||
}
|
||||
|
||||
Editor.propTypes = {
|
||||
rows: PropTypes.number, // TODO: should not be passed.
|
||||
id: PropTypes.string, // TODO: should not be passed.
|
||||
value: PropTypes.string,
|
||||
input: PropTypes.object,
|
||||
placeholder: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onInputChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
comment: PropTypes.object,
|
||||
classNames: PropTypes.object,
|
||||
registerHook: PropTypes.func,
|
||||
unregisterHook: PropTypes.func,
|
||||
isReply: PropTypes.bool,
|
||||
isEdit: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
|
||||
Reference in New Issue
Block a user