1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 12:56:41 +07:00

comment menu

This commit is contained in:
Fedor Katurov 2019-12-03 15:02:03 +07:00
parent ab898cc40c
commit 1bf9fe6b83
14 changed files with 319 additions and 98 deletions

View file

@ -3,18 +3,34 @@ import { CommentWrapper } from '~/components/containers/CommentWrapper';
import { ICommentGroup, IComment } from '~/redux/types'; import { ICommentGroup, IComment } from '~/redux/types';
import { CommentContent } from '~/components/node/CommentContent'; import { CommentContent } from '~/components/node/CommentContent';
import * as styles from './styles.scss'; import * as styles from './styles.scss';
import { nodeLockComment, nodeEditComment } from '~/redux/node/actions';
import { INodeState } from '~/redux/node/reducer';
import { CommentForm } from '../CommentForm';
type IProps = HTMLAttributes<HTMLDivElement> & { type IProps = HTMLAttributes<HTMLDivElement> & {
is_empty?: boolean; is_empty?: boolean;
is_loading?: boolean; is_loading?: boolean;
comment_group?: ICommentGroup; comment_group?: ICommentGroup;
comment_data: INodeState['comment_data'];
is_same?: boolean; is_same?: boolean;
can_edit?: boolean; can_edit?: boolean;
onDelete: (id: IComment['id'], is_deteted: boolean) => void; onDelete: typeof nodeLockComment;
onEdit: typeof nodeEditComment;
}; };
const Comment: FC<IProps> = memo( const Comment: FC<IProps> = memo(
({ comment_group, is_empty, is_same, is_loading, className, can_edit, onDelete, ...props }) => { ({
comment_group,
comment_data,
is_empty,
is_same,
is_loading,
className,
can_edit,
onDelete,
onEdit,
...props
}) => {
return ( return (
<CommentWrapper <CommentWrapper
className={className} className={className}
@ -25,18 +41,25 @@ const Comment: FC<IProps> = memo(
{...props} {...props}
> >
<div className={styles.wrap}> <div className={styles.wrap}>
{comment_group.comments.map(comment => {comment_group.comments.map(comment => {
comment.deleted_at ? ( if (comment.deleted_at) {
<div key={comment.id}>deleted</div> return <div key={comment.id}>deleted</div>;
) : ( }
if (Object.prototype.hasOwnProperty.call(comment_data, comment.id)) {
return <CommentForm id={comment.id} key={comment.id} />;
}
return (
<CommentContent <CommentContent
comment={comment} comment={comment}
key={comment.id} key={comment.id}
can_edit={can_edit} can_edit={can_edit}
onDelete={onDelete} onDelete={onDelete}
onEdit={onEdit}
/> />
) );
)} })}
</div> </div>
</CommentWrapper> </CommentWrapper>
); );

View file

@ -13,14 +13,17 @@ import classnames from 'classnames';
import { PRESETS } from '~/constants/urls'; import { PRESETS } from '~/constants/urls';
import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment'; import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment';
import { Icon } from '~/components/input/Icon'; import { Icon } from '~/components/input/Icon';
import { nodeLockComment, nodeEditComment } from '~/redux/node/actions';
import { CommentMenu } from '../CommentMenu';
interface IProps { interface IProps {
comment: IComment; comment: IComment;
can_edit: boolean; can_edit: boolean;
onDelete: (id: IComment['id'], is_deteted: boolean) => void; onDelete: typeof nodeLockComment;
onEdit: typeof nodeEditComment;
} }
const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete }) => { const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete, onEdit }) => {
const groupped = useMemo<Record<keyof typeof UPLOAD_TYPES, IFile[]>>( const groupped = useMemo<Record<keyof typeof UPLOAD_TYPES, IFile[]>>(
() => () =>
reduce( reduce(
@ -35,23 +38,20 @@ const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete }) => {
onDelete(comment.id, !comment.deleted_at); onDelete(comment.id, !comment.deleted_at);
}, [comment, onDelete]); }, [comment, onDelete]);
const lock = useMemo( const onEditClick = useCallback(() => {
() => onEdit(comment.id);
can_edit ? ( }, [comment, onEdit]);
<div className={styles.lock} onClick={onLockClick}>
<div> const menu = useMemo(
<Icon icon="close" /> () => can_edit && <CommentMenu onDelete={onLockClick} onEdit={onEditClick} />,
</div> [can_edit, comment, onEditClick, onLockClick]
</div>
) : null,
[can_edit, comment]
); );
return ( return (
<div className={styles.wrap}> <div className={styles.wrap}>
{comment.text && ( {comment.text && (
<Group className={classnames(styles.block, styles.block_text)}> <Group className={classnames(styles.block, styles.block_text)}>
{lock} {menu}
{formatCommentText(path(['user', 'username'], comment), comment.text).map( {formatCommentText(path(['user', 'username'], comment), comment.text).map(
(block, key) => (block, key) =>
@ -65,7 +65,7 @@ const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete }) => {
{groupped.image && groupped.image.length > 0 && ( {groupped.image && groupped.image.length > 0 && (
<div className={classnames(styles.block, styles.block_image)}> <div className={classnames(styles.block, styles.block_image)}>
{lock} {menu}
<div className={styles.images}> <div className={styles.images}>
{groupped.image.map(file => ( {groupped.image.map(file => (
@ -83,6 +83,8 @@ const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete }) => {
<> <>
{groupped.audio.map(file => ( {groupped.audio.map(file => (
<div className={classnames(styles.block, styles.block_audio)} key={file.id}> <div className={classnames(styles.block, styles.block_audio)} key={file.id}>
{menu}
<AudioPlayer file={file} /> <AudioPlayer file={file} />
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div> <div className={styles.date}>{getPrettyDate(comment.created_at)}</div>

View file

@ -4,7 +4,8 @@
position: relative; position: relative;
} }
.lock { .lock,
.edit {
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
@ -21,6 +22,7 @@
transition: opacity 0.25s, transform 0.25s; transition: opacity 0.25s, transform 0.25s;
cursor: pointer; cursor: pointer;
background: $red; background: $red;
z-index: 2;
& > div { & > div {
width: 20px; width: 20px;
@ -46,6 +48,11 @@
} }
} }
.edit {
top: 28px;
background: blue;
}
.block { .block {
@include outer_shadow(); @include outer_shadow();
min-height: $comment_height; min-height: $comment_height;
@ -68,7 +75,8 @@
} }
&:hover { &:hover {
.lock { .lock,
.edit {
opacity: 1; opacity: 1;
pointer-events: all; pointer-events: all;
touch-action: initial; touch-action: initial;

View file

@ -23,6 +23,7 @@ import { SortableImageGrid } from '~/components/editors/SortableImageGrid';
import { moveArrItem } from '~/utils/fn'; import { moveArrItem } from '~/utils/fn';
import { SortEnd } from 'react-sortable-hoc'; import { SortEnd } from 'react-sortable-hoc';
import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid'; import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
import { NodeCommentForm } from '../NodeCommentForm';
const mapStateToProps = (state: IState) => ({ const mapStateToProps = (state: IState) => ({
node: selectNode(state), node: selectNode(state),
@ -188,68 +189,66 @@ const CommentFormUnconnected: FC<IProps> = ({
); );
return ( return (
<CommentWrapper user={user}> <form onSubmit={onSubmit} className={styles.wrap}>
<form onSubmit={onSubmit} className={styles.wrap}> <div className={styles.input}>
<div className={styles.input}> <Textarea
<Textarea value={comment.text}
value={comment.text} handler={onInput}
handler={onInput} onKeyDown={onKeyDown}
onKeyDown={onKeyDown} disabled={is_sending_comment}
disabled={is_sending_comment} minRows={2}
minRows={2} />
/> </div>
{(!!images.length || !!audios.length) && (
<div className={styles.attaches}>
{!!images.length && (
<SortableImageGrid
onDrop={onFileDrop}
onSortEnd={onImageMove}
axis="xy"
items={images}
locked={locked_images}
pressDelay={50}
helperClass={styles.helper}
size={120}
/>
)}
{!!audios.length && (
<SortableAudioGrid
items={audios}
onDrop={onFileDrop}
onSortEnd={onAudioMove}
axis="y"
locked={[]}
pressDelay={50}
helperClass={styles.helper}
/>
)}
</div> </div>
)}
{(!!images.length || !!audios.length) && ( <Group horizontal className={styles.buttons}>
<div className={styles.attaches}> <ButtonGroup>
{!!images.length && ( <Button iconLeft="image" size="small" color="gray" iconOnly>
<SortableImageGrid <input type="file" onInput={onInputChange} multiple accept="image/*" />
onDrop={onFileDrop}
onSortEnd={onImageMove}
axis="xy"
items={images}
locked={locked_images}
pressDelay={50}
helperClass={styles.helper}
size={120}
/>
)}
{!!audios.length && (
<SortableAudioGrid
items={audios}
onDrop={onFileDrop}
onSortEnd={onAudioMove}
axis="y"
locked={[]}
pressDelay={50}
helperClass={styles.helper}
/>
)}
</div>
)}
<Group horizontal className={styles.buttons}>
<ButtonGroup>
<Button iconLeft="image" size="small" color="gray" iconOnly>
<input type="file" onInput={onInputChange} multiple accept="image/*" />
</Button>
<Button iconRight="enter" size="small" color="gray" iconOnly>
<input type="file" onInput={onInputChange} multiple accept="audio/*" />
</Button>
</ButtonGroup>
<Filler />
{is_sending_comment && <LoaderCircle size={20} />}
<Button size="small" color="gray" iconRight="enter" disabled={is_sending_comment}>
Сказать
</Button> </Button>
</Group>
</form> <Button iconRight="enter" size="small" color="gray" iconOnly>
</CommentWrapper> <input type="file" onInput={onInputChange} multiple accept="audio/*" />
</Button>
</ButtonGroup>
<Filler />
{is_sending_comment && <LoaderCircle size={20} />}
<Button size="small" color="gray" iconRight="enter" disabled={is_sending_comment}>
Сказать
</Button>
</Group>
</form>
); );
}; };

View file

@ -0,0 +1,20 @@
import React, { FC } from 'react';
import styles from './styles.scss';
interface IProps {
onEdit: () => void;
onDelete: () => void;
}
const CommentMenu: FC<IProps> = ({ onEdit, onDelete }) => {
return (
<div className={styles.wrap}>
<div className={styles.menu}>
<div className={styles.item}>Редактировать</div>
<div className={styles.item}>Удалить</div>
</div>
</div>
);
};
export { CommentMenu };

View file

@ -0,0 +1,68 @@
.wrap {
position: absolute;
right: 0;
top: 0;
width: 48px;
height: 48px;
z-index: 3;
&:hover {
.menu {
display: flex;
}
}
&::before {
content: ' ';
width: 10px;
height: 10px;
border-radius: 100%;
background: transparentize(black, 0.7);
position: absolute;
right: 5px;
top: 5px;
}
}
@keyframes appear {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.menu {
position: absolute;
right: 0;
top: 0;
display: none;
flex-direction: column;
z-index: 6;
animation: appear 0.25s forwards;
}
.item {
user-select: none;
font: $font_12_semibold;
text-transform: uppercase;
padding: $gap;
background: $content_bg;
cursor: pointer;
&:first-child {
border-top-left-radius: $radius;
border-top-right-radius: $radius;
}
&:last-child {
border-bottom-left-radius: $radius;
border-bottom-right-radius: $radius;
}
&:hover {
background: $primary;
}
}

View file

@ -0,0 +1,36 @@
import React, { FC, useCallback, KeyboardEventHandler, useEffect, useMemo } from 'react';
import { Textarea } from '~/components/input/Textarea';
import { CommentWrapper } from '~/components/containers/CommentWrapper';
import * as styles from './styles.scss';
import { Filler } from '~/components/containers/Filler';
import { Button } from '~/components/input/Button';
import assocPath from 'ramda/es/assocPath';
import { InputHandler, IFileWithUUID, IFile } from '~/redux/types';
import { connect } from 'react-redux';
import * as NODE_ACTIONS from '~/redux/node/actions';
import { selectNode } from '~/redux/node/selectors';
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
import { selectUploads } from '~/redux/uploads/selectors';
import { IState } from '~/redux/store';
import { selectUser, selectAuthUser } from '~/redux/auth/selectors';
import { CommentForm } from '../CommentForm';
const mapStateToProps = state => ({
user: selectAuthUser(state),
});
type IProps = ReturnType<typeof mapStateToProps> & {
is_before?: boolean;
};
const NodeCommentFormUnconnected: FC<IProps> = ({ user, is_before }) => {
return (
<CommentWrapper user={user}>
<CommentForm id={0} is_before={is_before} />
</CommentWrapper>
);
};
const NodeCommentForm = connect(mapStateToProps)(NodeCommentFormUnconnected);
export { NodeCommentForm };

View file

@ -7,14 +7,18 @@ import { ICommentGroup, IComment } from '~/redux/types';
import { groupCommentsByUser } from '~/utils/fn'; import { groupCommentsByUser } from '~/utils/fn';
import { IUser } from '~/redux/auth/types'; import { IUser } from '~/redux/auth/types';
import { canEditComment } from '~/utils/node'; import { canEditComment } from '~/utils/node';
import { nodeLockComment, nodeEditComment } from '~/redux/node/actions';
import { INodeState } from '~/redux/node/reducer';
interface IProps { interface IProps {
comments?: IComment[]; comments?: IComment[];
comment_data: INodeState['comment_data'];
user: IUser; user: IUser;
onDelete: (id: IComment['id'], is_deteted: boolean) => void; onDelete: typeof nodeLockComment;
onEdit: typeof nodeEditComment;
} }
const NodeComments: FC<IProps> = memo(({ comments, user, onDelete }) => { const NodeComments: FC<IProps> = memo(({ comments, comment_data, user, onDelete, onEdit }) => {
const groupped: ICommentGroup[] = useMemo(() => comments.reduce(groupCommentsByUser, []), [ const groupped: ICommentGroup[] = useMemo(() => comments.reduce(groupCommentsByUser, []), [
comments, comments,
]); ]);
@ -25,8 +29,10 @@ const NodeComments: FC<IProps> = memo(({ comments, user, onDelete }) => {
<Comment <Comment
key={group.ids.join()} key={group.ids.join()}
comment_group={group} comment_group={group}
comment_data={comment_data}
can_edit={canEditComment(group, user)} can_edit={canEditComment(group, user)}
onDelete={onDelete} onDelete={onDelete}
onEdit={onEdit}
/> />
))} ))}

View file

@ -11,6 +11,7 @@ import { Group } from '~/components/containers/Group';
import boris from '~/sprites/boris_robot.svg'; import boris from '~/sprites/boris_robot.svg';
import { NodeNoComments } from '~/components/node/NodeNoComments'; import { NodeNoComments } from '~/components/node/NodeNoComments';
import { getRandomPhrase } from '~/constants/phrases'; import { getRandomPhrase } from '~/constants/phrases';
import { NodeCommentForm } from '~/components/node/NodeCommentForm';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
node: selectNode(state), node: selectNode(state),
@ -20,6 +21,7 @@ const mapStateToProps = state => ({
const mapDispatchToProps = { const mapDispatchToProps = {
nodeLoadNode: NODE_ACTIONS.nodeLoadNode, nodeLoadNode: NODE_ACTIONS.nodeLoadNode,
nodeLockComment: NODE_ACTIONS.nodeLockComment, nodeLockComment: NODE_ACTIONS.nodeLockComment,
nodeEditComment: NODE_ACTIONS.nodeEditComment,
}; };
type IProps = ReturnType<typeof mapStateToProps> & type IProps = ReturnType<typeof mapStateToProps> &
@ -29,11 +31,12 @@ type IProps = ReturnType<typeof mapStateToProps> &
const id = 696; const id = 696;
const BorisLayoutUnconnected: FC<IProps> = ({ const BorisLayoutUnconnected: FC<IProps> = ({
node: { is_loading, is_loading_comments, comments = [] }, node: { is_loading, is_loading_comments, comments = [], comment_data },
user, user,
user: { is_user }, user: { is_user },
nodeLoadNode, nodeLoadNode,
nodeLockComment, nodeLockComment,
nodeEditComment,
}) => { }) => {
const title = getRandomPhrase('BORIS_TITLE'); const title = getRandomPhrase('BORIS_TITLE');
@ -77,12 +80,18 @@ const BorisLayoutUnconnected: FC<IProps> = ({
</div> </div>
<Group className={styles.content}> <Group className={styles.content}>
{is_user && <CommentForm id={0} is_before />} {is_user && <NodeCommentForm is_before />}
{is_loading_comments ? ( {is_loading_comments ? (
<NodeNoComments is_loading /> <NodeNoComments is_loading />
) : ( ) : (
<NodeComments comments={comments} user={user} onDelete={nodeLockComment} /> <NodeComments
comments={comments}
comment_data={comment_data}
user={user}
onDelete={nodeLockComment}
onEdit={nodeEditComment}
/>
)} )}
</Group> </Group>
</div> </div>

View file

@ -20,7 +20,7 @@ import { selectUser } from '~/redux/auth/selectors';
import pick from 'ramda/es/pick'; import pick from 'ramda/es/pick';
import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder'; import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder';
import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge'; import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge';
import { IComment } from '~/redux/types'; import { NodeCommentForm } from '~/components/node/NodeCommentForm';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
node: selectNode(state), node: selectNode(state),
@ -36,6 +36,7 @@ const mapDispatchToProps = {
nodeStar: NODE_ACTIONS.nodeStar, nodeStar: NODE_ACTIONS.nodeStar,
nodeLock: NODE_ACTIONS.nodeLock, nodeLock: NODE_ACTIONS.nodeLock,
nodeLockComment: NODE_ACTIONS.nodeLockComment, nodeLockComment: NODE_ACTIONS.nodeLockComment,
nodeEditComment: NODE_ACTIONS.nodeEditComment,
}; };
type IProps = ReturnType<typeof mapStateToProps> & type IProps = ReturnType<typeof mapStateToProps> &
@ -47,7 +48,7 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
match: { match: {
params: { id }, params: { id },
}, },
node: { is_loading, is_loading_comments, comments = [], current: node, related }, node: { is_loading, is_loading_comments, comments = [], current: node, related, comment_data },
user, user,
user: { is_user }, user: { is_user },
nodeGotoNode, nodeGotoNode,
@ -58,6 +59,7 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
nodeLock, nodeLock,
nodeSetCoverImage, nodeSetCoverImage,
nodeLockComment, nodeLockComment,
nodeEditComment,
}) => { }) => {
const [layout, setLayout] = useState({}); const [layout, setLayout] = useState({});
@ -131,10 +133,16 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
{is_loading || is_loading_comments || (!comments.length && !inline_block) ? ( {is_loading || is_loading_comments || (!comments.length && !inline_block) ? (
<NodeNoComments is_loading={is_loading_comments || is_loading} /> <NodeNoComments is_loading={is_loading_comments || is_loading} />
) : ( ) : (
<NodeComments comments={comments} user={user} onDelete={nodeLockComment} /> <NodeComments
comments={comments}
comment_data={comment_data}
user={user}
onDelete={nodeLockComment}
onEdit={nodeEditComment}
/>
)} )}
{is_user && !is_loading && <CommentForm id={0} />} {is_user && !is_loading && <NodeCommentForm />}
</Group> </Group>
<div className={styles.panel}> <div className={styles.panel}>

View file

@ -2,6 +2,11 @@ import { INode, IValidationErrors, IComment, ITag, IFile } from '../types';
import { NODE_ACTIONS, NODE_TYPES } from './constants'; import { NODE_ACTIONS, NODE_TYPES } from './constants';
import { INodeState } from './reducer'; import { INodeState } from './reducer';
export const nodeSet = (node: Partial<INodeState>) => ({
node,
type: NODE_ACTIONS.SET,
});
export const nodeSave = (node: INode) => ({ export const nodeSave = (node: INode) => ({
node, node,
type: NODE_ACTIONS.SAVE, type: NODE_ACTIONS.SAVE,
@ -109,6 +114,11 @@ export const nodeLockComment = (id: IComment['id'], is_locked: boolean) => ({
is_locked, is_locked,
}); });
export const nodeEditComment = (id: IComment['id']) => ({
type: NODE_ACTIONS.EDIT_COMMENT,
id,
});
export const nodeSetEditor = (editor: INode) => ({ export const nodeSetEditor = (editor: INode) => ({
type: NODE_ACTIONS.SET_EDITOR, type: NODE_ACTIONS.SET_EDITOR,
editor, editor,

View file

@ -19,12 +19,14 @@ export const NODE_ACTIONS = {
SAVE: `${prefix}SAVE`, SAVE: `${prefix}SAVE`,
LOAD_NODE: `${prefix}LOAD_NODE`, LOAD_NODE: `${prefix}LOAD_NODE`,
GOTO_NODE: `${prefix}GOTO_NODE`, GOTO_NODE: `${prefix}GOTO_NODE`,
SET: `${prefix}SET`,
EDIT: `${prefix}EDIT`, EDIT: `${prefix}EDIT`,
LIKE: `${prefix}LIKE`, LIKE: `${prefix}LIKE`,
STAR: `${prefix}STAR`, STAR: `${prefix}STAR`,
LOCK: `${prefix}LOCK`, LOCK: `${prefix}LOCK`,
LOCK_COMMENT: `${prefix}LOCK_COMMENT`, LOCK_COMMENT: `${prefix}LOCK_COMMENT`,
EDIT_COMMENT: `${prefix}EDIT_COMMENT`,
CREATE: `${prefix}CREATE`, CREATE: `${prefix}CREATE`,
SET_SAVE_ERRORS: `${prefix}SET_SAVE_ERRORS`, SET_SAVE_ERRORS: `${prefix}SET_SAVE_ERRORS`,

View file

@ -12,9 +12,15 @@ import {
nodeSetEditor, nodeSetEditor,
nodeSetCoverImage, nodeSetCoverImage,
nodeSetRelated, nodeSetRelated,
nodeSet,
} from './actions'; } from './actions';
import { INodeState } from './reducer'; import { INodeState } from './reducer';
const setData = (state: INodeState, { node }: ReturnType<typeof nodeSet>) => ({
...state,
...node,
});
const setSaveErrors = (state: INodeState, { errors }: ReturnType<typeof nodeSetSaveErrors>) => const setSaveErrors = (state: INodeState, { errors }: ReturnType<typeof nodeSetSaveErrors>) =>
assocPath(['errors'], errors, state); assocPath(['errors'], errors, state);
@ -57,6 +63,7 @@ const setCoverImage = (
) => assocPath(['current_cover_image'], current_cover_image, state); ) => assocPath(['current_cover_image'], current_cover_image, state);
export const NODE_HANDLERS = { export const NODE_HANDLERS = {
[NODE_ACTIONS.SET]: setData,
[NODE_ACTIONS.SET_SAVE_ERRORS]: setSaveErrors, [NODE_ACTIONS.SET_SAVE_ERRORS]: setSaveErrors,
[NODE_ACTIONS.SET_LOADING]: setLoading, [NODE_ACTIONS.SET_LOADING]: setLoading,
[NODE_ACTIONS.SET_LOADING_COMMENTS]: setLoadingComments, [NODE_ACTIONS.SET_LOADING_COMMENTS]: setLoadingComments,

View file

@ -1,5 +1,6 @@
import { takeLatest, call, put, select, delay, all } from 'redux-saga/effects'; import { takeLatest, call, put, select, delay, all } from 'redux-saga/effects';
import { push } from 'connected-react-router'; import { push } from 'connected-react-router';
import omit from 'ramda/es/omit';
import { NODE_ACTIONS, EMPTY_NODE, EMPTY_COMMENT, NODE_EDITOR_DATA } from './constants'; import { NODE_ACTIONS, EMPTY_NODE, EMPTY_COMMENT, NODE_EDITOR_DATA } from './constants';
import { import {
@ -23,6 +24,8 @@ import {
nodeGotoNode, nodeGotoNode,
nodeLock, nodeLock,
nodeLockComment, nodeLockComment,
nodeEditComment,
nodeSet,
} from './actions'; } from './actions';
import { import {
postNode, postNode,
@ -168,14 +171,23 @@ function* onPostComment({ id, is_before }: ReturnType<typeof nodePostComment>) {
const { current: current_node } = yield select(selectNode); const { current: current_node } = yield select(selectNode);
if (current_node && current_node.id === current.id) { if (current_node && current_node.id === current.id) {
// if user still browsing that node const { comments, comment_data } = yield select(selectNode);
const { comments } = yield select(selectNode);
yield put(nodeSetCommentData(0, { ...EMPTY_COMMENT }));
if (is_before) { if (id === 0) {
yield put(nodeSetComments([comment, ...comments])); yield put(nodeSetCommentData(0, { ...EMPTY_COMMENT }));
if (is_before) {
yield put(nodeSetComments([comment, ...comments]));
} else {
yield put(nodeSetComments([...comments, comment]));
}
} else { } else {
yield put(nodeSetComments([...comments, comment])); yield put(
nodeSet({
comment_data: omit([id.toString()], comment_data),
comments: comments.map(item => (item.id === id ? comment : item)),
})
);
} }
} }
} }
@ -279,6 +291,16 @@ function* onLockCommentSaga({ id, is_locked }: ReturnType<typeof nodeLockComment
yield call(reqWrapper, postNodeLockComment, { current: current.id, id, is_locked }); yield call(reqWrapper, postNodeLockComment, { current: current.id, id, is_locked });
} }
function* onEditCommentSaga({ id }: ReturnType<typeof nodeEditComment>) {
const { comments } = yield select(selectNode);
const comment = comments.find(item => item.id === id);
if (!comment) return;
yield put(nodeSetCommentData(id, { ...EMPTY_COMMENT, ...comment }));
}
export default function* nodeSaga() { export default function* nodeSaga() {
yield takeLatest(NODE_ACTIONS.SAVE, onNodeSave); yield takeLatest(NODE_ACTIONS.SAVE, onNodeSave);
yield takeLatest(NODE_ACTIONS.GOTO_NODE, onNodeGoto); yield takeLatest(NODE_ACTIONS.GOTO_NODE, onNodeGoto);
@ -291,4 +313,5 @@ export default function* nodeSaga() {
yield takeLatest(NODE_ACTIONS.STAR, onStarSaga); yield takeLatest(NODE_ACTIONS.STAR, onStarSaga);
yield takeLatest(NODE_ACTIONS.LOCK, onLockSaga); yield takeLatest(NODE_ACTIONS.LOCK, onLockSaga);
yield takeLatest(NODE_ACTIONS.LOCK_COMMENT, onLockCommentSaga); yield takeLatest(NODE_ACTIONS.LOCK_COMMENT, onLockCommentSaga);
yield takeLatest(NODE_ACTIONS.EDIT_COMMENT, onEditCommentSaga);
} }