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

#34 fixed boris layout with hooks

This commit is contained in:
Fedor Katurov 2021-02-27 18:55:58 +07:00
parent 29e5aef01b
commit 62f0fa59ca
19 changed files with 231 additions and 773 deletions

View file

@ -1,10 +1,8 @@
import React, { FC, HTMLAttributes, memo } from 'react'; import React, { FC, HTMLAttributes, memo } from 'react';
import { CommentWrapper } from '~/components/containers/CommentWrapper'; import { CommentWrapper } from '~/components/containers/CommentWrapper';
import { ICommentGroup } from '~/redux/types'; import { IComment, ICommentGroup } from '~/redux/types';
import { CommentContent } from '~/components/comment/CommentContent'; import { CommentContent } from '~/components/comment/CommentContent';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { nodeEditComment, nodeLockComment } from '~/redux/node/actions';
import { INodeState } from '~/redux/node/reducer';
import { CommendDeleted } from '../../node/CommendDeleted'; import { CommendDeleted } from '../../node/CommendDeleted';
import * as MODAL_ACTIONS from '~/redux/modal/actions'; import * as MODAL_ACTIONS from '~/redux/modal/actions';
@ -12,25 +10,21 @@ 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: typeof nodeLockComment; onDelete: (id: IComment['id'], isLocked: boolean) => void;
onEdit: typeof nodeEditComment;
modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe; modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe;
}; };
const Comment: FC<IProps> = memo( const Comment: FC<IProps> = memo(
({ ({
comment_group, comment_group,
comment_data,
is_empty, is_empty,
is_same, is_same,
is_loading, is_loading,
className, className,
can_edit, can_edit,
onDelete, onDelete,
onEdit,
modalShowPhotoswipe, modalShowPhotoswipe,
...props ...props
}) => { }) => {

View file

@ -10,17 +10,16 @@ import { AudioPlayer } from '~/components/media/AudioPlayer';
import classnames from 'classnames'; 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 { nodeLockComment } from '~/redux/node/actions';
import { CommentMenu } from '../CommentMenu'; import { CommentMenu } from '../CommentMenu';
import * as MODAL_ACTIONS from '~/redux/modal/actions'; import * as MODAL_ACTIONS from '~/redux/modal/actions';
import { LocalCommentForm } from '~/components/comment/LocalCommentForm'; import { CommentForm } from '~/components/comment/CommentForm';
import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
import { selectNode } from '~/redux/node/selectors'; import { selectNode } from '~/redux/node/selectors';
interface IProps { interface IProps {
comment: IComment; comment: IComment;
can_edit: boolean; can_edit: boolean;
onDelete: typeof nodeLockComment; onDelete: (id: IComment['id'], isLocked: boolean) => void;
modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe; modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe;
} }
@ -59,7 +58,7 @@ const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete, modalSho
); );
if (isEditing) { if (isEditing) {
return <LocalCommentForm nodeId={current.id} comment={comment} onCancelEdit={stopEditing} />; return <CommentForm nodeId={current.id} comment={comment} onCancelEdit={stopEditing} />;
} }
return ( return (

View file

@ -1,238 +1,97 @@
import React, { FC, KeyboardEventHandler, memo, useCallback, useEffect, useMemo, useState } from 'react'; import React, { FC, useCallback, useState } from 'react';
import { Textarea } from '~/components/input/Textarea'; import { useCommentFormFormik } from '~/utils/hooks/useCommentFormFormik';
import styles from './styles.module.scss'; import { FormikProvider } from 'formik';
import { Filler } from '~/components/containers/Filler'; import { LocalCommentFormTextarea } from '~/components/comment/LocalCommentFormTextarea';
import { Button } from '~/components/input/Button'; import { Button } from '~/components/input/Button';
import assocPath from 'ramda/es/assocPath'; import { FileUploaderProvider, useFileUploader } from '~/utils/hooks/fileUploader';
import { IComment, IFileWithUUID, InputHandler } from '~/redux/types'; import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
import { connect } from 'react-redux';
import * as NODE_ACTIONS from '~/redux/node/actions';
import { selectNode } from '~/redux/node/selectors';
import { LoaderCircle } from '~/components/input/LoaderCircle';
import { Group } from '~/components/containers/Group';
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from '~/redux/uploads/constants';
import uuid from 'uuid4';
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
import { selectUploads } from '~/redux/uploads/selectors';
import { IState } from '~/redux/store';
import { getFileType } from '~/utils/uploader';
import { useRandomPhrase } from '~/constants/phrases';
import { ERROR_LITERAL } from '~/constants/errors';
import { CommentFormAttaches } from '~/components/comment/CommentFormAttaches';
import { CommentFormAttachButtons } from '~/components/comment/CommentFormAttachButtons'; import { CommentFormAttachButtons } from '~/components/comment/CommentFormAttachButtons';
import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone';
import { CommentFormFormatButtons } from '~/components/comment/CommentFormFormatButtons'; import { CommentFormFormatButtons } from '~/components/comment/CommentFormFormatButtons';
import { CommentFormAttaches } from '~/components/comment/CommentFormAttaches';
import { LoaderCircle } from '~/components/input/LoaderCircle';
import { IComment, INode } from '~/redux/types';
import { EMPTY_COMMENT } from '~/redux/node/constants';
import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone';
import styles from './styles.module.scss';
import { ERROR_LITERAL } from '~/constants/errors';
import { Group } from '~/components/containers/Group';
const mapStateToProps = (state: IState) => ({ interface IProps {
node: selectNode(state), comment?: IComment;
uploads: selectUploads(state), nodeId: INode['id'];
}); onCancelEdit?: () => void;
}
const mapDispatchToProps = { const CommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
nodePostComment: NODE_ACTIONS.nodePostComment,
nodeCancelCommentEdit: NODE_ACTIONS.nodeCancelCommentEdit,
nodeSetCommentData: NODE_ACTIONS.nodeSetCommentData,
uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
};
type IProps = ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps & {
id: number;
is_before?: boolean;
};
const CommentFormUnconnected: FC<IProps> = memo(
({
node: { comment_data, is_sending_comment },
uploads: { statuses, files },
id,
is_before = false,
nodePostComment,
nodeSetCommentData,
uploadUploadFiles,
nodeCancelCommentEdit,
}) => {
const [textarea, setTextarea] = useState<HTMLTextAreaElement>(); const [textarea, setTextarea] = useState<HTMLTextAreaElement>();
const comment = useMemo(() => comment_data[id], [comment_data, id]); const uploader = useFileUploader(
UPLOAD_SUBJECTS.COMMENT,
const onUpload = useCallback( UPLOAD_TARGETS.COMMENTS,
(files: File[]) => { comment?.files
const items: IFileWithUUID[] = files.map(
(file: File): IFileWithUUID => ({
file,
temp_id: uuid(),
subject: UPLOAD_SUBJECTS.COMMENT,
target: UPLOAD_TARGETS.COMMENTS,
type: getFileType(file),
})
); );
const formik = useCommentFormFormik(comment || EMPTY_COMMENT, nodeId, uploader, onCancelEdit);
const isLoading = formik.isSubmitting || uploader.isUploading;
const isEditing = !!comment?.id;
const temps = items.map(file => file.temp_id); const clearError = useCallback(() => {
if (formik.status) {
formik.setStatus('');
}
nodeSetCommentData(id, assocPath(['temp_ids'], [...comment.temp_ids, ...temps], comment)); if (formik.errors.text) {
uploadUploadFiles(items); formik.setErrors({
}, ...formik.errors,
[uploadUploadFiles, comment, id, nodeSetCommentData] text: '',
);
const onInput = useCallback<InputHandler>(
text => {
nodeSetCommentData(id, assocPath(['text'], text, comment));
},
[nodeSetCommentData, comment, id]
);
useEffect(() => {
const temp_ids = (comment && comment.temp_ids) || [];
const added_files = temp_ids
.map(temp_uuid => statuses[temp_uuid] && statuses[temp_uuid].uuid)
.map(el => !!el && files[el])
.filter(el => !!el && !comment.files.some(file => file && file.id === el.id));
const filtered_temps = temp_ids.filter(
temp_id =>
statuses[temp_id] &&
(!statuses[temp_id].uuid || !added_files.some(file => file.id === statuses[temp_id].uuid))
);
if (added_files.length) {
nodeSetCommentData(id, {
...comment,
temp_ids: filtered_temps,
files: [...comment.files, ...added_files],
}); });
} }
}, [statuses, files]); }, [formik]);
const isUploadingNow = useMemo(() => comment.temp_ids.length > 0, [comment.temp_ids]); const error = formik.status || formik.errors.text;
const onSubmit = useCallback(
event => {
if (event) event.preventDefault();
if (isUploadingNow || is_sending_comment) return;
nodePostComment(id, is_before);
},
[nodePostComment, id, is_before, isUploadingNow, is_sending_comment]
);
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
({ ctrlKey, key }) => {
if (!!ctrlKey && key === 'Enter') onSubmit(null);
},
[onSubmit]
);
const images = useMemo(
() => comment.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE),
[comment.files]
);
const locked_images = useMemo(
() =>
comment.temp_ids
.filter(temp => statuses[temp] && statuses[temp].type === UPLOAD_TYPES.IMAGE)
.map(temp_id => statuses[temp_id]),
[statuses, comment.temp_ids]
);
const audios = useMemo(
() => comment.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO),
[comment.files]
);
const locked_audios = useMemo(
() =>
comment.temp_ids
.filter(temp => statuses[temp] && statuses[temp].type === UPLOAD_TYPES.AUDIO)
.map(temp_id => statuses[temp_id]),
[statuses, comment.temp_ids]
);
const onCancelEdit = useCallback(() => {
nodeCancelCommentEdit(id);
}, [nodeCancelCommentEdit, comment.id]);
const placeholder = useRandomPhrase('SIMPLE');
const clearError = useCallback(() => nodeSetCommentData(id, { error: '' }), [
id,
nodeSetCommentData,
]);
useEffect(() => {
if (comment.error) clearError();
}, [comment.files, comment.text]);
const setData = useCallback(
(data: Partial<IComment>) => {
nodeSetCommentData(id, data);
},
[nodeSetCommentData, id]
);
return ( return (
<> <CommentFormDropzone onUpload={uploader.uploadFiles}>
<CommentFormDropzone onUpload={onUpload}> <form onSubmit={formik.handleSubmit} className={styles.wrap}>
<form onSubmit={onSubmit} className={styles.wrap}> <FormikProvider value={formik}>
<FileUploaderProvider value={uploader}>
<div className={styles.input}> <div className={styles.input}>
<Textarea <LocalCommentFormTextarea setRef={setTextarea} />
value={comment.text}
handler={onInput}
onKeyDown={onKeyDown}
disabled={is_sending_comment}
placeholder={placeholder}
minRows={2}
setRef={setTextarea}
/>
{comment.error && ( {!!error && (
<div className={styles.error} onClick={clearError}> <div className={styles.error} onClick={clearError}>
{ERROR_LITERAL[comment.error] || comment.error} {ERROR_LITERAL[error] || error}
</div> </div>
)} )}
</div> </div>
<CommentFormAttaches <CommentFormAttaches />
images={images}
audios={audios}
locked_audios={locked_audios}
locked_images={locked_images}
comment={comment}
setComment={setData}
onUpload={onUpload}
/>
<Group horizontal className={styles.buttons}> <Group horizontal className={styles.buttons}>
<CommentFormAttachButtons onUpload={onUpload} /> <CommentFormAttachButtons onUpload={uploader.uploadFiles} />
<CommentFormFormatButtons element={textarea} handler={onInput} /> <CommentFormFormatButtons element={textarea} handler={formik.handleChange('text')} />
<Filler /> {isLoading && <LoaderCircle size={20} />}
{(is_sending_comment || isUploadingNow) && <LoaderCircle size={20} />} {isEditing && (
{id !== 0 && (
<Button size="small" color="link" type="button" onClick={onCancelEdit}> <Button size="small" color="link" type="button" onClick={onCancelEdit}>
Отмена Отмена
</Button> </Button>
)} )}
<Button <Button
type="submit"
size="small" size="small"
color="gray" color="gray"
iconRight={id === 0 ? 'enter' : 'check'} iconRight={!isEditing ? 'enter' : 'check'}
disabled={is_sending_comment || isUploadingNow} disabled={isLoading}
> >
{id === 0 ? 'Сказать' : 'Сохранить'} {!isEditing ? 'Сказать' : 'Сохранить'}
</Button> </Button>
</Group> </Group>
</FileUploaderProvider>
</FormikProvider>
</form> </form>
</CommentFormDropzone> </CommentFormDropzone>
</>
);
}
); );
};
const CommentForm = connect(mapStateToProps, mapDispatchToProps)(CommentFormUnconnected); export { CommentForm };
export { CommentForm, CommentFormUnconnected };

View file

@ -27,6 +27,7 @@
padding: $gap / 2; padding: $gap / 2;
border-radius: 0 0 $radius $radius; border-radius: 0 0 $radius $radius;
flex-wrap: wrap; flex-wrap: wrap;
} }
.uploads { .uploads {

View file

@ -1,106 +1,83 @@
import React, { FC, useCallback } from 'react'; import React, { FC, useCallback, useMemo } from 'react';
import styles from '~/components/comment/CommentForm/styles.module.scss'; import styles from './styles.module.scss';
import { SortableImageGrid } from '~/components/editors/SortableImageGrid'; import { SortableImageGrid } from '~/components/editors/SortableImageGrid';
import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid'; import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
import { IComment, IFile } from '~/redux/types'; import { IFile } from '~/redux/types';
import { IUploadStatus } from '~/redux/uploads/reducer';
import { SortEnd } from 'react-sortable-hoc'; import { SortEnd } from 'react-sortable-hoc';
import assocPath from 'ramda/es/assocPath';
import { moveArrItem } from '~/utils/fn'; import { moveArrItem } from '~/utils/fn';
import { useDropZone } from '~/utils/hooks'; import { useDropZone } from '~/utils/hooks';
import { COMMENT_FILE_TYPES } from '~/redux/uploads/constants'; import { COMMENT_FILE_TYPES, UPLOAD_TYPES } from '~/redux/uploads/constants';
import { useFileUploaderContext } from '~/utils/hooks/fileUploader';
interface IProps { const CommentFormAttaches: FC = () => {
images: IFile[]; const { files, pending, setFiles, uploadFiles } = useFileUploaderContext();
audios: IFile[];
locked_images: IUploadStatus[];
locked_audios: IUploadStatus[];
comment: IComment;
setComment: (data: IComment) => void;
onUpload: (files: File[]) => void;
}
const CommentFormAttaches: FC<IProps> = ({ const images = useMemo(() => files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [
images, files,
audios, ]);
locked_images,
locked_audios,
comment,
setComment,
onUpload,
}) => {
const onDrop = useDropZone(onUpload, COMMENT_FILE_TYPES);
const hasImageAttaches = images.length > 0 || locked_images.length > 0; const pendingImages = useMemo(() => pending.filter(item => item.type === UPLOAD_TYPES.IMAGE), [
const hasAudioAttaches = audios.length > 0 || locked_audios.length > 0; pending,
]);
const audios = useMemo(() => files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), [
files,
]);
const pendingAudios = useMemo(() => pending.filter(item => item.type === UPLOAD_TYPES.AUDIO), [
pending,
]);
const onDrop = useDropZone(uploadFiles, COMMENT_FILE_TYPES);
const hasImageAttaches = images.length > 0 || pendingImages.length > 0;
const hasAudioAttaches = audios.length > 0 || pendingAudios.length > 0;
const hasAttaches = hasImageAttaches || hasAudioAttaches; const hasAttaches = hasImageAttaches || hasAudioAttaches;
const onImageMove = useCallback( const onImageMove = useCallback(
({ oldIndex, newIndex }: SortEnd) => { ({ oldIndex, newIndex }: SortEnd) => {
setComment( setFiles([
assocPath(
['files'],
[
...audios, ...audios,
...(moveArrItem( ...(moveArrItem(
oldIndex, oldIndex,
newIndex, newIndex,
images.filter(file => !!file) images.filter(file => !!file)
) as IFile[]), ) as IFile[]),
], ]);
comment
)
);
}, },
[images, audios, comment, setComment] [images, audios, setFiles]
);
const onFileDelete = useCallback(
(fileId: IFile['id']) => {
setComment(
assocPath(
['files'],
comment.files.filter(file => file.id != fileId),
comment
)
);
},
[setComment, comment]
);
const onTitleChange = useCallback(
(fileId: IFile['id'], title: IFile['metadata']['title']) => {
setComment(
assocPath(
['files'],
comment.files.map(file =>
file.id === fileId ? { ...file, metadata: { ...file.metadata, title } } : file
),
comment
)
);
},
[comment, setComment]
); );
const onAudioMove = useCallback( const onAudioMove = useCallback(
({ oldIndex, newIndex }: SortEnd) => { ({ oldIndex, newIndex }: SortEnd) => {
setComment( setFiles([
assocPath(
['files'],
[
...images, ...images,
...(moveArrItem( ...(moveArrItem(
oldIndex, oldIndex,
newIndex, newIndex,
audios.filter(file => !!file) audios.filter(file => !!file)
) as IFile[]), ) as IFile[]),
], ]);
comment },
[images, audios, setFiles]
);
const onFileDelete = useCallback(
(fileId: IFile['id']) => {
setFiles(files.filter(file => file.id !== fileId));
},
[setFiles, files]
);
const onAudioTitleChange = useCallback(
(fileId: IFile['id'], title: IFile['metadata']['title']) => {
setFiles(
files.map(file =>
file.id === fileId ? { ...file, metadata: { ...file.metadata, title } } : file
) )
); );
}, },
[images, audios, comment, setComment] [files, setFiles]
); );
return ( return (
@ -112,7 +89,7 @@ const CommentFormAttaches: FC<IProps> = ({
onSortEnd={onImageMove} onSortEnd={onImageMove}
axis="xy" axis="xy"
items={images} items={images}
locked={locked_images} locked={pendingImages}
pressDelay={50} pressDelay={50}
helperClass={styles.helper} helperClass={styles.helper}
size={120} size={120}
@ -123,10 +100,10 @@ const CommentFormAttaches: FC<IProps> = ({
<SortableAudioGrid <SortableAudioGrid
items={audios} items={audios}
onDelete={onFileDelete} onDelete={onFileDelete}
onTitleChange={onTitleChange} onTitleChange={onAudioTitleChange}
onSortEnd={onAudioMove} onSortEnd={onAudioMove}
axis="y" axis="y"
locked={locked_audios} locked={pendingAudios}
pressDelay={50} pressDelay={50}
helperClass={styles.helper} helperClass={styles.helper}
/> />

View file

@ -0,0 +1,5 @@
@import "src/styles/variables";
.attaches {
@include outer_shadow();
}

View file

@ -1,97 +0,0 @@
import React, { FC, useCallback, useState } from 'react';
import { useCommentFormFormik } from '~/utils/hooks/useCommentFormFormik';
import { FormikProvider } from 'formik';
import { LocalCommentFormTextarea } from '~/components/comment/LocalCommentFormTextarea';
import { Button } from '~/components/input/Button';
import { FileUploaderProvider, useFileUploader } from '~/utils/hooks/fileUploader';
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
import { CommentFormAttachButtons } from '~/components/comment/CommentFormAttachButtons';
import { CommentFormFormatButtons } from '~/components/comment/CommentFormFormatButtons';
import { LocalCommentFormAttaches } from '~/components/comment/LocalCommentFormAttaches';
import { LoaderCircle } from '~/components/input/LoaderCircle';
import { IComment, INode } from '~/redux/types';
import { EMPTY_COMMENT } from '~/redux/node/constants';
import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone';
import styles from './styles.module.scss';
import { ERROR_LITERAL } from '~/constants/errors';
import { Group } from '~/components/containers/Group';
interface IProps {
comment?: IComment;
nodeId: INode['id'];
onCancelEdit?: () => void;
}
const LocalCommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
const [textarea, setTextarea] = useState<HTMLTextAreaElement>();
const uploader = useFileUploader(
UPLOAD_SUBJECTS.COMMENT,
UPLOAD_TARGETS.COMMENTS,
comment?.files
);
const formik = useCommentFormFormik(comment || EMPTY_COMMENT, nodeId, uploader, onCancelEdit);
const isLoading = formik.isSubmitting || uploader.isUploading;
const isEditing = !!comment?.id;
const clearError = useCallback(() => {
if (formik.status) {
formik.setStatus('');
}
if (formik.errors.text) {
formik.setErrors({
...formik.errors,
text: '',
});
}
}, [formik]);
const error = formik.status || formik.errors.text;
return (
<CommentFormDropzone onUpload={uploader.uploadFiles}>
<form onSubmit={formik.handleSubmit} className={styles.wrap}>
<FormikProvider value={formik}>
<FileUploaderProvider value={uploader}>
<div className={styles.input}>
<LocalCommentFormTextarea setRef={setTextarea} />
{!!error && (
<div className={styles.error} onClick={clearError}>
{ERROR_LITERAL[error] || error}
</div>
)}
</div>
<LocalCommentFormAttaches />
<Group horizontal className={styles.buttons}>
<CommentFormAttachButtons onUpload={uploader.uploadFiles} />
<CommentFormFormatButtons element={textarea} handler={formik.handleChange('text')} />
{isLoading && <LoaderCircle size={20} />}
{isEditing && (
<Button size="small" color="link" type="button" onClick={onCancelEdit}>
Отмена
</Button>
)}
<Button
type="submit"
size="small"
color="gray"
iconRight={!isEditing ? 'enter' : 'check'}
disabled={isLoading}
>
{!isEditing ? 'Сказать' : 'Сохранить'}
</Button>
</Group>
</FileUploaderProvider>
</FormikProvider>
</form>
</CommentFormDropzone>
);
};
export { LocalCommentForm };

View file

@ -1,57 +0,0 @@
@import "src/styles/variables";
.wrap {
display: flex;
flex-direction: column;
textarea {
min-height: 62px !important;
}
}
.input {
@include outer_shadow();
position: relative;
flex: 1;
padding: ($gap / 2) ($gap / 2 + 1px);
}
.buttons {
@include outer_shadow();
position: relative;
z-index: 1;
display: flex;
flex-direction: row;
background: transparentize(black, 0.8);
padding: $gap / 2;
border-radius: 0 0 $radius $radius;
flex-wrap: wrap;
}
.uploads {
padding: ($gap / 2);
display: grid;
grid-column-gap: $gap / 2;
grid-row-gap: $gap / 2;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}
.attaches {
@include outer_shadow();
}
.error {
position: absolute;
bottom: 0;
left: 50%;
background: $red;
z-index: 10;
font: $font_12_regular;
box-sizing: border-box;
padding: 0 $gap;
border-radius: 4px 4px 0 0;
transform: translate(-50%, 0);
cursor: pointer;
}

View file

@ -1,116 +0,0 @@
import React, { FC, useCallback, useMemo } from 'react';
import styles from '~/components/comment/CommentForm/styles.module.scss';
import { SortableImageGrid } from '~/components/editors/SortableImageGrid';
import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
import { IFile } from '~/redux/types';
import { SortEnd } from 'react-sortable-hoc';
import { moveArrItem } from '~/utils/fn';
import { useDropZone } from '~/utils/hooks';
import { COMMENT_FILE_TYPES, UPLOAD_TYPES } from '~/redux/uploads/constants';
import { useFileUploaderContext } from '~/utils/hooks/fileUploader';
const LocalCommentFormAttaches: FC = () => {
const { files, pending, setFiles, uploadFiles } = useFileUploaderContext();
const images = useMemo(() => files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [
files,
]);
const pendingImages = useMemo(() => pending.filter(item => item.type === UPLOAD_TYPES.IMAGE), [
pending,
]);
const audios = useMemo(() => files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), [
files,
]);
const pendingAudios = useMemo(() => pending.filter(item => item.type === UPLOAD_TYPES.AUDIO), [
pending,
]);
const onDrop = useDropZone(uploadFiles, COMMENT_FILE_TYPES);
const hasImageAttaches = images.length > 0 || pendingImages.length > 0;
const hasAudioAttaches = audios.length > 0 || pendingAudios.length > 0;
const hasAttaches = hasImageAttaches || hasAudioAttaches;
const onImageMove = useCallback(
({ oldIndex, newIndex }: SortEnd) => {
setFiles([
...audios,
...(moveArrItem(
oldIndex,
newIndex,
images.filter(file => !!file)
) as IFile[]),
]);
},
[images, audios, setFiles]
);
const onAudioMove = useCallback(
({ oldIndex, newIndex }: SortEnd) => {
setFiles([
...images,
...(moveArrItem(
oldIndex,
newIndex,
audios.filter(file => !!file)
) as IFile[]),
]);
},
[images, audios, setFiles]
);
const onFileDelete = useCallback(
(fileId: IFile['id']) => {
setFiles(files.filter(file => file.id !== fileId));
},
[setFiles, files]
);
const onAudioTitleChange = useCallback(
(fileId: IFile['id'], title: IFile['metadata']['title']) => {
setFiles(
files.map(file =>
file.id === fileId ? { ...file, metadata: { ...file.metadata, title } } : file
)
);
},
[files, setFiles]
);
return (
hasAttaches && (
<div className={styles.attaches} onDropCapture={onDrop}>
{hasImageAttaches && (
<SortableImageGrid
onDelete={onFileDelete}
onSortEnd={onImageMove}
axis="xy"
items={images}
locked={pendingImages}
pressDelay={50}
helperClass={styles.helper}
size={120}
/>
)}
{hasAudioAttaches && (
<SortableAudioGrid
items={audios}
onDelete={onFileDelete}
onTitleChange={onAudioTitleChange}
onSortEnd={onAudioMove}
axis="y"
locked={pendingAudios}
pressDelay={50}
helperClass={styles.helper}
/>
)}
</div>
)
);
};
export { LocalCommentFormAttaches };

View file

@ -1,12 +1,11 @@
import React, { FC, useCallback } from 'react'; import React, { FC, useCallback } from 'react';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { Button } from '~/components/input/Button'; import { Button } from '~/components/input/Button';
import { nodeLockComment } from '~/redux/node/actions';
import { IComment } from '~/redux/types'; import { IComment } from '~/redux/types';
interface IProps { interface IProps {
id: IComment['id']; id: IComment['id'];
onDelete: typeof nodeLockComment; onDelete: (id: IComment['id'], isLocked: boolean) => void;
} }
const CommendDeleted: FC<IProps> = ({ id, onDelete }) => { const CommendDeleted: FC<IProps> = ({ id, onDelete }) => {

View file

@ -2,8 +2,7 @@ import React, { FC } from 'react';
import { CommentWrapper } from '~/components/containers/CommentWrapper'; import { CommentWrapper } from '~/components/containers/CommentWrapper';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectAuthUser } from '~/redux/auth/selectors'; import { selectAuthUser } from '~/redux/auth/selectors';
import { CommentForm } from '../../comment/CommentForm'; import { CommentForm } from '~/components/comment/CommentForm';
import { LocalCommentForm } from '~/components/comment/LocalCommentForm';
import { INode } from '~/redux/types'; import { INode } from '~/redux/types';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
@ -18,8 +17,7 @@ type IProps = ReturnType<typeof mapStateToProps> & {
const NodeCommentFormUnconnected: FC<IProps> = ({ user, isBefore, nodeId }) => { const NodeCommentFormUnconnected: FC<IProps> = ({ user, isBefore, nodeId }) => {
return ( return (
<CommentWrapper user={user}> <CommentWrapper user={user}>
<CommentForm id={0} is_before={isBefore} /> <CommentForm nodeId={nodeId} />
<LocalCommentForm nodeId={nodeId} />
</CommentWrapper> </CommentWrapper>
); );
}; };

View file

@ -1,67 +1,54 @@
import React, { FC, useMemo, memo } from 'react'; import React, { FC, memo, useCallback, useMemo } from 'react';
import { Comment } from '../../comment/Comment'; import { Comment } from '../../comment/Comment';
import { Filler } from '~/components/containers/Filler';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { ICommentGroup, IComment } from '~/redux/types'; import { IComment, ICommentGroup, IFile } 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, nodeLoadMoreComments } from '~/redux/node/actions'; import { nodeLoadMoreComments, nodeLockComment } from '~/redux/node/actions';
import { INodeState } from '~/redux/node/reducer'; import { INodeState } from '~/redux/node/reducer';
import { COMMENTS_DISPLAY } from '~/redux/node/constants'; import { COMMENTS_DISPLAY } from '~/redux/node/constants';
import { plural } from '~/utils/dom'; import { plural } from '~/utils/dom';
import * as MODAL_ACTIONS from '~/redux/modal/actions'; import { modalShowPhotoswipe } from '~/redux/modal/actions';
import { useDispatch } from 'react-redux';
interface IProps { interface IProps {
comments?: IComment[]; comments?: IComment[];
comment_data: INodeState['comment_data']; count: INodeState['comment_count'];
comment_count: INodeState['comment_count'];
user: IUser; user: IUser;
onDelete: typeof nodeLockComment;
onEdit: typeof nodeEditComment;
onLoadMore: typeof nodeLoadMoreComments;
order?: 'ASC' | 'DESC'; order?: 'ASC' | 'DESC';
modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe;
} }
const NodeComments: FC<IProps> = memo( const NodeComments: FC<IProps> = memo(({ comments, user, count = 0, order = 'DESC' }) => {
({ const dispatch = useDispatch();
comments, const left = useMemo(() => Math.max(0, count - comments.length), [comments, count]);
comment_data,
user,
onDelete,
onEdit,
onLoadMore,
comment_count = 0,
order = 'DESC',
modalShowPhotoswipe,
}) => {
const comments_left = useMemo(() => Math.max(0, comment_count - comments.length), [
comments,
comment_count,
]);
const groupped: ICommentGroup[] = useMemo( const groupped: ICommentGroup[] = useMemo(
() => (order === 'DESC' ? [...comments].reverse() : comments).reduce(groupCommentsByUser, []), () => (order === 'DESC' ? [...comments].reverse() : comments).reduce(groupCommentsByUser, []),
[comments, order] [comments, order]
); );
const onDelete = useCallback(
(id: IComment['id'], locked: boolean) => dispatch(nodeLockComment(id, locked)),
[dispatch]
);
const onLoadMoreComments = useCallback(() => dispatch(nodeLoadMoreComments()), [dispatch]);
const onShowPhotoswipe = useCallback(
(images: IFile[], index: number) => dispatch(modalShowPhotoswipe(images, index)),
[dispatch]
);
const more = useMemo( const more = useMemo(
() => () =>
comments_left > 0 && ( left > 0 && (
<div className={styles.more} onClick={onLoadMore}> <div className={styles.more} onClick={onLoadMoreComments}>
Показать ещё{' '} Показать ещё{' '}
{plural( {plural(Math.min(left, COMMENTS_DISPLAY), 'комментарий', 'комментария', 'комментариев')}
Math.min(comments_left, COMMENTS_DISPLAY), {left > COMMENTS_DISPLAY ? ` из ${left} оставшихся` : ''}
'комментарий',
'комментария',
'комментариев'
)}
{comments_left > COMMENTS_DISPLAY ? ` из ${comments_left} оставшихся` : ''}
</div> </div>
), ),
[comments_left, onLoadMore, COMMENTS_DISPLAY] [left, onLoadMoreComments]
); );
return ( return (
@ -72,18 +59,15 @@ const NodeComments: FC<IProps> = memo(
<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} modalShowPhotoswipe={onShowPhotoswipe}
modalShowPhotoswipe={modalShowPhotoswipe}
/> />
))} ))}
{order === 'ASC' && more} {order === 'ASC' && more}
</div> </div>
); );
} });
);
export { NodeComments }; export { NodeComments };

View file

@ -1,8 +1,7 @@
import React, { FC, useEffect } from 'react'; import React, { FC, useEffect } from 'react';
import { RouteComponentProps } from 'react-router'; import { selectNode, selectNodeComments } from '~/redux/node/selectors';
import { selectNode } from '~/redux/node/selectors';
import { selectUser } from '~/redux/auth/selectors'; import { selectUser } from '~/redux/auth/selectors';
import { connect } from 'react-redux'; import { useDispatch } from 'react-redux';
import { NodeComments } from '~/components/node/NodeComments'; import { NodeComments } from '~/components/node/NodeComments';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { Group } from '~/components/containers/Group'; import { Group } from '~/components/containers/Group';
@ -10,72 +9,49 @@ import boris from '~/sprites/boris_robot.svg';
import { NodeNoComments } from '~/components/node/NodeNoComments'; import { NodeNoComments } from '~/components/node/NodeNoComments';
import { useRandomPhrase } from '~/constants/phrases'; import { useRandomPhrase } from '~/constants/phrases';
import { NodeCommentForm } from '~/components/node/NodeCommentForm'; import { NodeCommentForm } from '~/components/node/NodeCommentForm';
import * as NODE_ACTIONS from '~/redux/node/actions';
import * as AUTH_ACTIONS from '~/redux/auth/actions';
import * as MODAL_ACTIONS from '~/redux/modal/actions';
import * as BORIS_ACTIONS from '~/redux/boris/actions';
import isBefore from 'date-fns/isBefore'; import isBefore from 'date-fns/isBefore';
import { Card } from '~/components/containers/Card'; import { Card } from '~/components/containers/Card';
import { Footer } from '~/components/main/Footer'; import { Footer } from '~/components/main/Footer';
import { Sticky } from '~/components/containers/Sticky'; import { Sticky } from '~/components/containers/Sticky';
import { selectBorisStats } from '~/redux/boris/selectors';
import { BorisStats } from '~/components/boris/BorisStats'; import { BorisStats } from '~/components/boris/BorisStats';
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
import { selectBorisStats } from '~/redux/boris/selectors';
import { authSetUser } from '~/redux/auth/actions';
import { nodeLoadNode } from '~/redux/node/actions';
import { borisLoadStats } from '~/redux/boris/actions';
const mapStateToProps = state => ({ type IProps = {};
node: selectNode(state),
user: selectUser(state),
stats: selectBorisStats(state),
});
const mapDispatchToProps = { const BorisLayout: FC<IProps> = () => {
nodeLoadNode: NODE_ACTIONS.nodeLoadNode,
nodeLockComment: NODE_ACTIONS.nodeLockComment,
nodeEditComment: NODE_ACTIONS.nodeEditComment,
nodeLoadMoreComments: NODE_ACTIONS.nodeLoadMoreComments,
authSetUser: AUTH_ACTIONS.authSetUser,
modalShowPhotoswipe: MODAL_ACTIONS.modalShowPhotoswipe,
borisLoadStats: BORIS_ACTIONS.borisLoadStats,
};
type IProps = ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps &
RouteComponentProps<{ id: string }> & {};
const id = 696;
const BorisLayoutUnconnected: FC<IProps> = ({
node: { is_loading, is_loading_comments, comments = [], comment_data, comment_count, id },
user,
user: { is_user, last_seen_boris },
nodeLoadNode,
nodeLockComment,
nodeEditComment,
nodeLoadMoreComments,
modalShowPhotoswipe,
authSetUser,
borisLoadStats,
stats,
}) => {
const title = useRandomPhrase('BORIS_TITLE'); const title = useRandomPhrase('BORIS_TITLE');
const dispatch = useDispatch();
const node = useShallowSelect(selectNode);
const user = useShallowSelect(selectUser);
const stats = useShallowSelect(selectBorisStats);
const comments = useShallowSelect(selectNodeComments);
useEffect(() => { useEffect(() => {
const last_comment = comments[0]; const last_comment = comments[0];
if (!last_comment) return; if (!last_comment) return;
if (last_seen_boris && !isBefore(new Date(last_seen_boris), new Date(last_comment.created_at)))
if (
user.last_seen_boris &&
!isBefore(new Date(user.last_seen_boris), new Date(last_comment.created_at))
)
return; return;
authSetUser({ last_seen_boris: last_comment.created_at }); dispatch(authSetUser({ last_seen_boris: last_comment.created_at }));
}, [comments, last_seen_boris]); }, [user.last_seen_boris, dispatch, comments]);
useEffect(() => { useEffect(() => {
if (is_loading) return; if (node.is_loading) return;
nodeLoadNode(id, 'DESC'); dispatch(nodeLoadNode(696, 'DESC'));
}, [nodeLoadNode, id]); }, [dispatch]);
useEffect(() => { useEffect(() => {
borisLoadStats(); dispatch(borisLoadStats());
}, [borisLoadStats]); }, [dispatch]);
return ( return (
<div className={styles.wrap}> <div className={styles.wrap}>
@ -92,20 +68,15 @@ const BorisLayoutUnconnected: FC<IProps> = ({
<div className={styles.container}> <div className={styles.container}>
<Card className={styles.content}> <Card className={styles.content}>
<Group className={styles.grid}> <Group className={styles.grid}>
{is_user && <NodeCommentForm isBefore nodeId={id} />} {user.is_user && <NodeCommentForm isBefore nodeId={node.current.id} />}
{is_loading_comments ? ( {node.is_loading_comments ? (
<NodeNoComments is_loading count={7} /> <NodeNoComments is_loading count={7} />
) : ( ) : (
<NodeComments <NodeComments
comments={comments} comments={comments}
comment_data={comment_data} count={node.comment_count}
comment_count={comment_count}
user={user} user={user}
onDelete={nodeLockComment}
onEdit={nodeEditComment}
onLoadMore={nodeLoadMoreComments}
modalShowPhotoswipe={modalShowPhotoswipe}
order="ASC" order="ASC"
/> />
)} )}
@ -139,6 +110,4 @@ const BorisLayoutUnconnected: FC<IProps> = ({
); );
}; };
const BorisLayout = connect(mapStateToProps, mapDispatchToProps)(BorisLayoutUnconnected);
export { BorisLayout }; export { BorisLayout };

View file

@ -181,14 +181,9 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
<NodeNoComments is_loading={is_loading_comments || is_loading} /> <NodeNoComments is_loading={is_loading_comments || is_loading} />
) : ( ) : (
<NodeComments <NodeComments
count={comment_count}
comments={comments} comments={comments}
comment_data={comment_data}
comment_count={comment_count}
user={user} user={user}
onDelete={nodeLockComment}
onEdit={nodeEditComment}
onLoadMore={nodeLoadMoreComments}
modalShowPhotoswipe={modalShowPhotoswipe}
order="DESC" order="DESC"
/> />
)} )}

View file

@ -3,11 +3,11 @@ import { RouteComponentProps, useRouteMatch, withRouter } from 'react-router';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { NodeNoComments } from '~/components/node/NodeNoComments'; import { NodeNoComments } from '~/components/node/NodeNoComments';
import { Grid } from '~/components/containers/Grid'; import { Grid } from '~/components/containers/Grid';
import { CommentForm } from '~/components/comment/CommentForm';
import * as NODE_ACTIONS from '~/redux/node/actions'; import * as NODE_ACTIONS from '~/redux/node/actions';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { IUser } from '~/redux/auth/types'; import { IUser } from '~/redux/auth/types';
import { Group } from '~/components/containers/Group'; import { Group } from '~/components/containers/Group';
import { CommentForm } from '~/components/comment/CommentForm';
const mapStateToProps = () => ({}); const mapStateToProps = () => ({});
const mapDispatchToProps = { const mapDispatchToProps = {
@ -39,7 +39,7 @@ const ProfileLayoutUnconnected: FC<IProps> = ({ history, nodeSetCoverImage }) =>
<Grid className={styles.content}> <Grid className={styles.content}>
<div className={styles.comments}> <div className={styles.comments}>
<CommentForm id={0} /> <CommentForm nodeId={0} />
<NodeNoComments is_loading={false} /> <NodeNoComments is_loading={false} />
</div> </div>
</Grid> </Grid>

View file

@ -44,12 +44,6 @@ export const nodeSetCurrent = (current: INodeState['current']) => ({
type: NODE_ACTIONS.SET_CURRENT, type: NODE_ACTIONS.SET_CURRENT,
}); });
export const nodePostComment = (id: number, is_before: boolean) => ({
id,
is_before,
type: NODE_ACTIONS.POST_COMMENT,
});
export const nodePostLocalComment = ( export const nodePostLocalComment = (
nodeId: INode['id'], nodeId: INode['id'],
comment: IComment, comment: IComment,
@ -58,7 +52,7 @@ export const nodePostLocalComment = (
nodeId, nodeId,
comment, comment,
callback, callback,
type: NODE_ACTIONS.POST_LOCAL_COMMENT, type: NODE_ACTIONS.POST_COMMENT,
}); });
export const nodeCancelCommentEdit = (id: number) => ({ export const nodeCancelCommentEdit = (id: number) => ({

View file

@ -41,8 +41,7 @@ export const NODE_ACTIONS = {
SET_COMMENT_DATA: `${prefix}SET_COMMENT_DATA`, SET_COMMENT_DATA: `${prefix}SET_COMMENT_DATA`,
SET_EDITOR: `${prefix}SET_EDITOR`, SET_EDITOR: `${prefix}SET_EDITOR`,
POST_COMMENT: `${prefix}POST_COMMENT`, POST_COMMENT: `${prefix}POST_LOCAL_COMMENT`,
POST_LOCAL_COMMENT: `${prefix}POST_LOCAL_COMMENT`,
SET_COMMENTS: `${prefix}SET_COMMENTS`, SET_COMMENTS: `${prefix}SET_COMMENTS`,
SET_RELATED: `${prefix}SET_RELATED`, SET_RELATED: `${prefix}SET_RELATED`,

View file

@ -13,7 +13,6 @@ import {
nodeLoadNode, nodeLoadNode,
nodeLock, nodeLock,
nodeLockComment, nodeLockComment,
nodePostComment,
nodePostLocalComment, nodePostLocalComment,
nodeSave, nodeSave,
nodeSet, nodeSet,
@ -25,7 +24,6 @@ import {
nodeSetLoadingComments, nodeSetLoadingComments,
nodeSetRelated, nodeSetRelated,
nodeSetSaveErrors, nodeSetSaveErrors,
nodeSetSendingComment,
nodeSetTags, nodeSetTags,
nodeUpdateTags nodeUpdateTags
} from './actions'; } from './actions';
@ -191,44 +189,7 @@ function* onNodeLoad({ id, order = 'ASC' }: ReturnType<typeof nodeLoadNode>) {
return; return;
} }
function* onPostComment({ id }: ReturnType<typeof nodePostComment>) { function* onPostComment({ nodeId, comment, callback }: ReturnType<typeof nodePostLocalComment>) {
const { current, comment_data } = yield select(selectNode);
yield put(nodeSetSendingComment(true));
const {
data: { comment },
error,
} = yield call(reqWrapper, postNodeComment, { data: comment_data[id], id: current.id });
yield put(nodeSetSendingComment(false));
if (error || !comment) {
return yield put(nodeSetCommentData(id, { error }));
}
const { current: current_node } = yield select(selectNode);
if (current_node && current_node.id === current.id) {
const { comments, comment_data: current_comment_data } = yield select(selectNode);
if (id === 0) {
yield put(nodeSetCommentData(0, { ...EMPTY_COMMENT }));
yield put(nodeSetComments([comment, ...comments]));
} else {
yield put(
nodeSet({
comment_data: omit([id.toString()], current_comment_data),
comments: comments.map(item => (item.id === id ? comment : item)),
})
);
}
}
}
function* onPostLocalComment({
nodeId,
comment,
callback,
}: ReturnType<typeof nodePostLocalComment>) {
const { data, error }: Unwrap<ReturnType<typeof postNodeComment>> = yield call( const { data, error }: Unwrap<ReturnType<typeof postNodeComment>> = yield call(
reqWrapper, reqWrapper,
postNodeComment, postNodeComment,
@ -390,7 +351,6 @@ export default function* nodeSaga() {
yield takeLatest(NODE_ACTIONS.GOTO_NODE, onNodeGoto); yield takeLatest(NODE_ACTIONS.GOTO_NODE, onNodeGoto);
yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad); yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad);
yield takeLatest(NODE_ACTIONS.POST_COMMENT, onPostComment); yield takeLatest(NODE_ACTIONS.POST_COMMENT, onPostComment);
yield takeLatest(NODE_ACTIONS.POST_LOCAL_COMMENT, onPostLocalComment);
yield takeLatest(NODE_ACTIONS.CANCEL_COMMENT_EDIT, onCancelCommentEdit); yield takeLatest(NODE_ACTIONS.CANCEL_COMMENT_EDIT, onCancelCommentEdit);
yield takeLatest(NODE_ACTIONS.UPDATE_TAGS, onUpdateTags); yield takeLatest(NODE_ACTIONS.UPDATE_TAGS, onUpdateTags);
yield takeLatest(NODE_ACTIONS.CREATE, onCreateSaga); yield takeLatest(NODE_ACTIONS.CREATE, onCreateSaga);

View file

@ -1,10 +1,5 @@
import { IState } from '../store'; import { IState } from '../store';
import { INodeState } from './reducer';
import { IResultWithStatus, INode } from '../types';
export const selectNode = (state: IState): INodeState => state.node; export const selectNode = (state: IState) => state.node;
export const selectNodeComments = (state: IState) => state.node.comments;
// export const catchNodeErrors = (data: IResultWithStatus<INode>): IResultWithStatus<INode> => ({ export const selectNodeCurrent = (state: IState) => state.node.current;
// data,
// errors: data.errors,
// })