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:
parent
ab898cc40c
commit
1bf9fe6b83
14 changed files with 319 additions and 98 deletions
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
20
src/components/node/CommentMenu/index.tsx
Normal file
20
src/components/node/CommentMenu/index.tsx
Normal 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 };
|
68
src/components/node/CommentMenu/styles.scss
Normal file
68
src/components/node/CommentMenu/styles.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
36
src/components/node/NodeCommentForm/index.tsx
Normal file
36
src/components/node/NodeCommentForm/index.tsx
Normal 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 };
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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`,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue