mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
Merge branch 'develop'
This commit is contained in:
commit
5d3e598a02
27 changed files with 698 additions and 674 deletions
|
@ -14,6 +14,7 @@
|
||||||
"connected-react-router": "^6.5.2",
|
"connected-react-router": "^6.5.2",
|
||||||
"date-fns": "^2.4.1",
|
"date-fns": "^2.4.1",
|
||||||
"flexbin": "^0.2.0",
|
"flexbin": "^0.2.0",
|
||||||
|
"formik": "^2.2.6",
|
||||||
"insane": "^2.6.2",
|
"insane": "^2.6.2",
|
||||||
"marked": "^2.0.0",
|
"marked": "^2.0.0",
|
||||||
"node-sass": "4.14.1",
|
"node-sass": "4.14.1",
|
||||||
|
@ -23,7 +24,7 @@
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-popper": "^2.2.3",
|
"react-popper": "^2.2.3",
|
||||||
"react-redux": "^6.0.1",
|
"react-redux": "^7.2.2",
|
||||||
"react-router": "^5.1.2",
|
"react-router": "^5.1.2",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scripts": "3.4.4",
|
"react-scripts": "3.4.4",
|
||||||
|
@ -36,7 +37,8 @@
|
||||||
"throttle-debounce": "^2.1.0",
|
"throttle-debounce": "^2.1.0",
|
||||||
"typescript": "^4.0.5",
|
"typescript": "^4.0.5",
|
||||||
"uuid4": "^1.1.4",
|
"uuid4": "^1.1.4",
|
||||||
"web-vitals": "^0.2.4"
|
"web-vitals": "^0.2.4",
|
||||||
|
"yup": "^0.32.9"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "craco start",
|
"start": "craco start",
|
||||||
|
@ -68,6 +70,7 @@
|
||||||
"@types/node": "^11.13.22",
|
"@types/node": "^11.13.22",
|
||||||
"@types/ramda": "^0.26.33",
|
"@types/ramda": "^0.26.33",
|
||||||
"@types/react-redux": "^7.1.11",
|
"@types/react-redux": "^7.1.11",
|
||||||
|
"@types/yup": "^0.29.11",
|
||||||
"craco-alias": "^2.1.1",
|
"craco-alias": "^2.1.1",
|
||||||
"craco-fast-refresh": "^1.0.2",
|
"craco-fast-refresh": "^1.0.2",
|
||||||
"prettier": "^1.18.2"
|
"prettier": "^1.18.2"
|
||||||
|
|
|
@ -1,11 +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 { CommentForm } from '../CommentForm';
|
|
||||||
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';
|
||||||
|
|
||||||
|
@ -13,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
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -50,17 +43,12 @@ const Comment: FC<IProps> = memo(
|
||||||
return <CommendDeleted id={comment.id} onDelete={onDelete} key={comment.id} />;
|
return <CommendDeleted id={comment.id} onDelete={onDelete} key={comment.id} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(comment_data, comment.id)) {
|
|
||||||
return <CommentForm id={comment.id} key={comment.id} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
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}
|
|
||||||
modalShowPhotoswipe={modalShowPhotoswipe}
|
modalShowPhotoswipe={modalShowPhotoswipe}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,112 +1,115 @@
|
||||||
import React, { FC, useMemo, memo, createElement, useCallback, Fragment } from 'react';
|
import React, { createElement, FC, Fragment, memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { IComment, IFile } from '~/redux/types';
|
import { IComment, IFile } from '~/redux/types';
|
||||||
import { path } from 'ramda';
|
import { append, assocPath, path } from 'ramda';
|
||||||
import { formatCommentText, getURL, getPrettyDate } from '~/utils/dom';
|
import { formatCommentText, getPrettyDate, getURL } from '~/utils/dom';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
import { assocPath } from 'ramda';
|
|
||||||
import { append } from 'ramda';
|
|
||||||
import reduce from 'ramda/es/reduce';
|
import reduce from 'ramda/es/reduce';
|
||||||
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
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, nodeEditComment } 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 { CommentForm } from '~/components/comment/CommentForm';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
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;
|
||||||
onEdit: typeof nodeEditComment;
|
|
||||||
modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe;
|
modalShowPhotoswipe: typeof MODAL_ACTIONS.modalShowPhotoswipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommentContent: FC<IProps> = memo(
|
const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete, modalShowPhotoswipe }) => {
|
||||||
({ comment, can_edit, onDelete, onEdit, modalShowPhotoswipe }) => {
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const groupped = useMemo<Record<keyof typeof UPLOAD_TYPES, IFile[]>>(
|
const { current } = useShallowSelect(selectNode);
|
||||||
() =>
|
|
||||||
reduce(
|
|
||||||
(group, file) => assocPath([file.type], append(file, group[file.type]), group),
|
|
||||||
{},
|
|
||||||
comment.files
|
|
||||||
),
|
|
||||||
[comment]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLockClick = useCallback(() => {
|
const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]);
|
||||||
onDelete(comment.id, !comment.deleted_at);
|
const stopEditing = useCallback(() => setIsEditing(false), [setIsEditing]);
|
||||||
}, [comment, onDelete]);
|
|
||||||
|
|
||||||
const onEditClick = useCallback(() => {
|
const groupped = useMemo<Record<keyof typeof UPLOAD_TYPES, IFile[]>>(
|
||||||
onEdit(comment.id);
|
() =>
|
||||||
}, [comment, onEdit]);
|
reduce(
|
||||||
|
(group, file) => assocPath([file.type], append(file, group[file.type]), group),
|
||||||
|
{},
|
||||||
|
comment.files
|
||||||
|
),
|
||||||
|
[comment]
|
||||||
|
);
|
||||||
|
|
||||||
const menu = useMemo(
|
const onLockClick = useCallback(() => {
|
||||||
() => can_edit && <CommentMenu onDelete={onLockClick} onEdit={onEditClick} />,
|
onDelete(comment.id, !comment.deleted_at);
|
||||||
[can_edit, comment, onEditClick, onLockClick]
|
}, [comment, onDelete]);
|
||||||
);
|
|
||||||
|
|
||||||
const blocks = useMemo(
|
const menu = useMemo(
|
||||||
() =>
|
() => can_edit && <CommentMenu onDelete={onLockClick} onEdit={startEditing} />,
|
||||||
!!comment.text.trim()
|
[can_edit, startEditing, onLockClick]
|
||||||
? formatCommentText(path(['user', 'username'], comment), comment.text)
|
);
|
||||||
: [],
|
|
||||||
[comment.text]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
const blocks = useMemo(
|
||||||
<div className={styles.wrap}>
|
() =>
|
||||||
{comment.text && (
|
!!comment.text.trim()
|
||||||
<Group className={classnames(styles.block, styles.block_text)}>
|
? formatCommentText(path(['user', 'username'], comment), comment.text)
|
||||||
{menu}
|
: [],
|
||||||
|
[comment]
|
||||||
|
);
|
||||||
|
|
||||||
<Group className={styles.renderers}>
|
if (isEditing) {
|
||||||
{blocks.map(
|
return <CommentForm nodeId={current.id} comment={comment} onCancelEdit={stopEditing} />;
|
||||||
(block, key) =>
|
}
|
||||||
COMMENT_BLOCK_RENDERERS[block.type] &&
|
|
||||||
createElement(COMMENT_BLOCK_RENDERERS[block.type], { block, key })
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
|
return (
|
||||||
|
<div className={styles.wrap}>
|
||||||
|
{comment.text && (
|
||||||
|
<Group className={classnames(styles.block, styles.block_text)}>
|
||||||
|
{menu}
|
||||||
|
|
||||||
|
<Group className={styles.renderers}>
|
||||||
|
{blocks.map(
|
||||||
|
(block, key) =>
|
||||||
|
COMMENT_BLOCK_RENDERERS[block.type] &&
|
||||||
|
createElement(COMMENT_BLOCK_RENDERERS[block.type], { block, key })
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
|
||||||
|
|
||||||
{groupped.image && groupped.image.length > 0 && (
|
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
|
||||||
<div className={classnames(styles.block, styles.block_image)}>
|
</Group>
|
||||||
{menu}
|
)}
|
||||||
|
|
||||||
<div className={styles.images}>
|
{groupped.image && groupped.image.length > 0 && (
|
||||||
{groupped.image.map((file, index) => (
|
<div className={classnames(styles.block, styles.block_image)}>
|
||||||
<div key={file.id} onClick={() => modalShowPhotoswipe(groupped.image, index)}>
|
{menu}
|
||||||
<img src={getURL(file, PRESETS['600'])} alt={file.name} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
|
<div className={styles.images}>
|
||||||
</div>
|
{groupped.image.map((file, index) => (
|
||||||
)}
|
<div key={file.id} onClick={() => modalShowPhotoswipe(groupped.image, index)}>
|
||||||
|
<img src={getURL(file, PRESETS['600'])} alt={file.name} />
|
||||||
{groupped.audio && groupped.audio.length > 0 && (
|
|
||||||
<Fragment>
|
|
||||||
{groupped.audio.map(file => (
|
|
||||||
<div className={classnames(styles.block, styles.block_audio)} key={file.id}>
|
|
||||||
{menu}
|
|
||||||
|
|
||||||
<AudioPlayer file={file} />
|
|
||||||
|
|
||||||
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Fragment>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
|
||||||
);
|
</div>
|
||||||
}
|
)}
|
||||||
);
|
|
||||||
|
{groupped.audio && groupped.audio.length > 0 && (
|
||||||
|
<Fragment>
|
||||||
|
{groupped.audio.map(file => (
|
||||||
|
<div className={classnames(styles.block, styles.block_audio)} key={file.id}>
|
||||||
|
{menu}
|
||||||
|
|
||||||
|
<AudioPlayer file={file} />
|
||||||
|
|
||||||
|
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export { CommentContent };
|
export { CommentContent };
|
||||||
|
|
|
@ -1,244 +1,97 @@
|
||||||
import React, {
|
import React, { FC, useCallback, useState } from 'react';
|
||||||
FC,
|
import { useCommentFormFormik } from '~/utils/hooks/useCommentFormFormik';
|
||||||
KeyboardEventHandler,
|
import { FormikProvider } from 'formik';
|
||||||
memo,
|
import { LocalCommentFormTextarea } from '~/components/comment/LocalCommentFormTextarea';
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { Textarea } from '~/components/input/Textarea';
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
import { Filler } from '~/components/containers/Filler';
|
|
||||||
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,
|
const [textarea, setTextarea] = useState<HTMLTextAreaElement>();
|
||||||
nodeCancelCommentEdit: NODE_ACTIONS.nodeCancelCommentEdit,
|
const uploader = useFileUploader(
|
||||||
nodeSetCommentData: NODE_ACTIONS.nodeSetCommentData,
|
UPLOAD_SUBJECTS.COMMENT,
|
||||||
uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
|
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>
|
||||||
|
|
||||||
|
<CommentFormAttaches />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type IProps = ReturnType<typeof mapStateToProps> &
|
export { CommentForm };
|
||||||
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 comment = useMemo(() => comment_data[id], [comment_data, id]);
|
|
||||||
|
|
||||||
const onUpload = useCallback(
|
|
||||||
(files: File[]) => {
|
|
||||||
const items: IFileWithUUID[] = files.map(
|
|
||||||
(file: File): IFileWithUUID => ({
|
|
||||||
file,
|
|
||||||
temp_id: uuid(),
|
|
||||||
subject: UPLOAD_SUBJECTS.COMMENT,
|
|
||||||
target: UPLOAD_TARGETS.COMMENTS,
|
|
||||||
type: getFileType(file),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const temps = items.map(file => file.temp_id);
|
|
||||||
|
|
||||||
nodeSetCommentData(id, assocPath(['temp_ids'], [...comment.temp_ids, ...temps], comment));
|
|
||||||
uploadUploadFiles(items);
|
|
||||||
},
|
|
||||||
[uploadUploadFiles, comment, id, nodeSetCommentData]
|
|
||||||
);
|
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
const isUploadingNow = useMemo(() => comment.temp_ids.length > 0, [comment.temp_ids]);
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<CommentFormDropzone onUpload={onUpload}>
|
|
||||||
<form onSubmit={onSubmit} className={styles.wrap}>
|
|
||||||
<div className={styles.input}>
|
|
||||||
<Textarea
|
|
||||||
value={comment.text}
|
|
||||||
handler={onInput}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
disabled={is_sending_comment}
|
|
||||||
placeholder={placeholder}
|
|
||||||
minRows={2}
|
|
||||||
setRef={setTextarea}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{comment.error && (
|
|
||||||
<div className={styles.error} onClick={clearError}>
|
|
||||||
{ERROR_LITERAL[comment.error] || comment.error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CommentFormAttaches
|
|
||||||
images={images}
|
|
||||||
audios={audios}
|
|
||||||
locked_audios={locked_audios}
|
|
||||||
locked_images={locked_images}
|
|
||||||
comment={comment}
|
|
||||||
setComment={setData}
|
|
||||||
onUpload={onUpload}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Group horizontal className={styles.buttons}>
|
|
||||||
<CommentFormAttachButtons onUpload={onUpload} />
|
|
||||||
<CommentFormFormatButtons element={textarea} handler={onInput} />
|
|
||||||
|
|
||||||
<Filler />
|
|
||||||
|
|
||||||
{(is_sending_comment || isUploadingNow) && <LoaderCircle size={20} />}
|
|
||||||
|
|
||||||
{id !== 0 && (
|
|
||||||
<Button size="small" color="link" type="button" onClick={onCancelEdit}>
|
|
||||||
Отмена
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
color="gray"
|
|
||||||
iconRight={id === 0 ? 'enter' : 'check'}
|
|
||||||
disabled={is_sending_comment || isUploadingNow}
|
|
||||||
>
|
|
||||||
{id === 0 ? 'Сказать' : 'Сохранить'}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</form>
|
|
||||||
</CommentFormDropzone>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const CommentForm = connect(mapStateToProps, mapDispatchToProps)(CommentFormUnconnected);
|
|
||||||
|
|
||||||
export { CommentForm, CommentFormUnconnected };
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(
|
...audios,
|
||||||
['files'],
|
...(moveArrItem(
|
||||||
[
|
oldIndex,
|
||||||
...audios,
|
newIndex,
|
||||||
...(moveArrItem(
|
images.filter(file => !!file)
|
||||||
oldIndex,
|
) as IFile[]),
|
||||||
newIndex,
|
]);
|
||||||
images.filter(file => !!file)
|
|
||||||
) 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(
|
...images,
|
||||||
['files'],
|
...(moveArrItem(
|
||||||
[
|
oldIndex,
|
||||||
...images,
|
newIndex,
|
||||||
...(moveArrItem(
|
audios.filter(file => !!file)
|
||||||
oldIndex,
|
) as IFile[]),
|
||||||
newIndex,
|
]);
|
||||||
audios.filter(file => !!file)
|
},
|
||||||
) as IFile[]),
|
[images, audios, setFiles]
|
||||||
],
|
);
|
||||||
comment
|
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
@import "src/styles/variables";
|
||||||
|
|
||||||
|
.attaches {
|
||||||
|
@include outer_shadow();
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
@import "~/styles/variables.scss";
|
@import '~/styles/variables.scss';
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
@media(max-width: 480px) {
|
@media(max-width: 480px) {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
36
src/components/comment/LocalCommentFormTextarea/index.tsx
Normal file
36
src/components/comment/LocalCommentFormTextarea/index.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import React, { FC, KeyboardEventHandler, useCallback } from 'react';
|
||||||
|
import { Textarea } from '~/components/input/Textarea';
|
||||||
|
import { useCommentFormContext } from '~/utils/hooks/useCommentFormFormik';
|
||||||
|
import { useRandomPhrase } from '~/constants/phrases';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
isLoading?: boolean;
|
||||||
|
setRef?: (r: HTMLTextAreaElement) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LocalCommentFormTextarea: FC<IProps> = ({ setRef }) => {
|
||||||
|
const { values, handleChange, handleSubmit, isSubmitting } = useCommentFormContext();
|
||||||
|
|
||||||
|
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
|
||||||
|
({ ctrlKey, key }) => {
|
||||||
|
if (!!ctrlKey && key === 'Enter') handleSubmit(null);
|
||||||
|
},
|
||||||
|
[handleSubmit]
|
||||||
|
);
|
||||||
|
|
||||||
|
const placeholder = useRandomPhrase('SIMPLE');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
value={values.text}
|
||||||
|
handler={handleChange('text')}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
placeholder={placeholder}
|
||||||
|
minRows={2}
|
||||||
|
setRef={setRef}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LocalCommentFormTextarea };
|
|
@ -178,17 +178,6 @@
|
||||||
fill: white;
|
fill: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> * {
|
|
||||||
margin: 0 5px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.micro {
|
.micro {
|
||||||
|
|
|
@ -59,14 +59,21 @@ const Textarea = memo<IProps>(
|
||||||
const onFocus = useCallback(() => setFocused(true), [setFocused]);
|
const onFocus = useCallback(() => setFocused(true), [setFocused]);
|
||||||
const onBlur = useCallback(() => setFocused(false), [setFocused]);
|
const onBlur = useCallback(() => setFocused(false), [setFocused]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const target = ref?.current;
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
autosize(target);
|
||||||
|
setRef(target);
|
||||||
|
|
||||||
|
return () => autosize.destroy(target);
|
||||||
|
}, [ref, setRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
|
|
||||||
autosize(ref.current);
|
autosize.update(ref.current);
|
||||||
setRef(ref.current);
|
}, [value]);
|
||||||
|
|
||||||
return () => autosize.destroy(ref.current);
|
|
||||||
}, [ref.current]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -89,7 +96,7 @@ const Textarea = memo<IProps>(
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
style={{
|
style={{
|
||||||
maxHeight: maxRows * 20,
|
// maxHeight: maxRows * 20,
|
||||||
minHeight: minRows * 20,
|
minHeight: minRows * 20,
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -1,32 +1,23 @@
|
||||||
import React, { FC, useCallback, KeyboardEventHandler, useEffect, useMemo } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Textarea } from '~/components/input/Textarea';
|
|
||||||
import { CommentWrapper } from '~/components/containers/CommentWrapper';
|
import { CommentWrapper } from '~/components/containers/CommentWrapper';
|
||||||
import styles from './styles.module.scss';
|
|
||||||
import { Filler } from '~/components/containers/Filler';
|
|
||||||
import { Button } from '~/components/input/Button';
|
|
||||||
import { assocPath } from 'ramda';
|
|
||||||
import { InputHandler, IFileWithUUID, IFile } from '~/redux/types';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import * as NODE_ACTIONS from '~/redux/node/actions';
|
import { selectAuthUser } from '~/redux/auth/selectors';
|
||||||
import { selectNode } from '~/redux/node/selectors';
|
import { CommentForm } from '~/components/comment/CommentForm';
|
||||||
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
import { INode } from '~/redux/types';
|
||||||
import { selectUploads } from '~/redux/uploads/selectors';
|
|
||||||
import { IState } from '~/redux/store';
|
|
||||||
import { selectUser, selectAuthUser } from '~/redux/auth/selectors';
|
|
||||||
import { CommentForm } from '../../comment/CommentForm';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
user: selectAuthUser(state),
|
user: selectAuthUser(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
type IProps = ReturnType<typeof mapStateToProps> & {
|
type IProps = ReturnType<typeof mapStateToProps> & {
|
||||||
is_before?: boolean;
|
isBefore?: boolean;
|
||||||
|
nodeId: INode['id'];
|
||||||
};
|
};
|
||||||
|
|
||||||
const NodeCommentFormUnconnected: FC<IProps> = ({ user, is_before }) => {
|
const NodeCommentFormUnconnected: FC<IProps> = ({ user, isBefore, nodeId }) => {
|
||||||
return (
|
return (
|
||||||
<CommentWrapper user={user}>
|
<CommentWrapper user={user}>
|
||||||
<CommentForm id={0} is_before={is_before} />
|
<CommentForm nodeId={nodeId} />
|
||||||
</CommentWrapper>
|
</CommentWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,89 +1,73 @@
|
||||||
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 more = useMemo(
|
const onDelete = useCallback(
|
||||||
() =>
|
(id: IComment['id'], locked: boolean) => dispatch(nodeLockComment(id, locked)),
|
||||||
comments_left > 0 && (
|
[dispatch]
|
||||||
<div className={styles.more} onClick={onLoadMore}>
|
);
|
||||||
Показать ещё{' '}
|
const onLoadMoreComments = useCallback(() => dispatch(nodeLoadMoreComments()), [dispatch]);
|
||||||
{plural(
|
const onShowPhotoswipe = useCallback(
|
||||||
Math.min(comments_left, COMMENTS_DISPLAY),
|
(images: IFile[], index: number) => dispatch(modalShowPhotoswipe(images, index)),
|
||||||
'комментарий',
|
[dispatch]
|
||||||
'комментария',
|
);
|
||||||
'комментариев'
|
|
||||||
)}
|
|
||||||
{comments_left > COMMENTS_DISPLAY ? ` из ${comments_left} оставшихся` : ''}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
[comments_left, onLoadMore, COMMENTS_DISPLAY]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
const more = useMemo(
|
||||||
<div className={styles.wrap}>
|
() =>
|
||||||
{order === 'DESC' && more}
|
left > 0 && (
|
||||||
|
<div className={styles.more} onClick={onLoadMoreComments}>
|
||||||
|
Показать ещё{' '}
|
||||||
|
{plural(Math.min(left, COMMENTS_DISPLAY), 'комментарий', 'комментария', 'комментариев')}
|
||||||
|
{left > COMMENTS_DISPLAY ? ` из ${left} оставшихся` : ''}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[left, onLoadMoreComments]
|
||||||
|
);
|
||||||
|
|
||||||
{groupped.map(group => (
|
return (
|
||||||
<Comment
|
<div className={styles.wrap}>
|
||||||
key={group.ids.join()}
|
{order === 'DESC' && more}
|
||||||
comment_group={group}
|
|
||||||
comment_data={comment_data}
|
|
||||||
can_edit={canEditComment(group, user)}
|
|
||||||
onDelete={onDelete}
|
|
||||||
onEdit={onEdit}
|
|
||||||
modalShowPhotoswipe={modalShowPhotoswipe}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{order === 'ASC' && more}
|
{groupped.map(group => (
|
||||||
</div>
|
<Comment
|
||||||
);
|
key={group.ids.join()}
|
||||||
}
|
comment_group={group}
|
||||||
);
|
can_edit={canEditComment(group, user)}
|
||||||
|
onDelete={onDelete}
|
||||||
|
modalShowPhotoswipe={onShowPhotoswipe}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{order === 'ASC' && more}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export { NodeComments };
|
export { NodeComments };
|
||||||
|
|
|
@ -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 },
|
|
||||||
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 is_before />}
|
{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 };
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { NodeNoComments } from '~/components/node/NodeNoComments';
|
||||||
import { NodeRelated } from '~/components/node/NodeRelated';
|
import { NodeRelated } from '~/components/node/NodeRelated';
|
||||||
import { NodeComments } from '~/components/node/NodeComments';
|
import { NodeComments } from '~/components/node/NodeComments';
|
||||||
import { NodeTags } from '~/components/node/NodeTags';
|
import { NodeTags } from '~/components/node/NodeTags';
|
||||||
import { INodeComponentProps, NODE_COMPONENTS, NODE_HEADS, NODE_INLINES, } from '~/redux/node/constants';
|
import { INodeComponentProps, NODE_COMPONENTS, NODE_HEADS, NODE_INLINES } from '~/redux/node/constants';
|
||||||
import { selectUser } from '~/redux/auth/selectors';
|
import { selectUser } from '~/redux/auth/selectors';
|
||||||
import { pick } from 'ramda';
|
import { pick } from 'ramda';
|
||||||
import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder';
|
import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder';
|
||||||
|
@ -30,6 +30,7 @@ import { selectModal } from '~/redux/modal/selectors';
|
||||||
import { SidebarRouter } from '~/containers/main/SidebarRouter';
|
import { SidebarRouter } from '~/containers/main/SidebarRouter';
|
||||||
import { ITag } from '~/redux/types';
|
import { ITag } from '~/redux/types';
|
||||||
import { URLS } from '~/constants/urls';
|
import { URLS } from '~/constants/urls';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
|
||||||
const mapStateToProps = (state: IState) => ({
|
const mapStateToProps = (state: IState) => ({
|
||||||
node: selectNode(state),
|
node: selectNode(state),
|
||||||
|
@ -60,15 +61,6 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
||||||
match: {
|
match: {
|
||||||
params: { id },
|
params: { id },
|
||||||
},
|
},
|
||||||
node: {
|
|
||||||
is_loading,
|
|
||||||
is_loading_comments,
|
|
||||||
comments = [],
|
|
||||||
current: node,
|
|
||||||
related,
|
|
||||||
comment_data,
|
|
||||||
comment_count,
|
|
||||||
},
|
|
||||||
modal: { is_shown: is_modal_shown },
|
modal: { is_shown: is_modal_shown },
|
||||||
user,
|
user,
|
||||||
user: { is_user },
|
user: { is_user },
|
||||||
|
@ -86,7 +78,15 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
|
||||||
}) => {
|
}) => {
|
||||||
const [layout, setLayout] = useState({});
|
const [layout, setLayout] = useState({});
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const {
|
||||||
|
is_loading,
|
||||||
|
is_loading_comments,
|
||||||
|
comments = [],
|
||||||
|
current: node,
|
||||||
|
related,
|
||||||
|
comment_data,
|
||||||
|
comment_count,
|
||||||
|
} = useShallowSelect(selectNode);
|
||||||
const updateLayout = useCallback(() => setLayout({}), []);
|
const updateLayout = useCallback(() => setLayout({}), []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -181,19 +181,14 @@ 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"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{is_user && !is_loading && <NodeCommentForm />}
|
{is_user && !is_loading && <NodeCommentForm nodeId={node.id} />}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<div className={styles.panel}>
|
<div className={styles.panel}>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { INode, IValidationErrors, IComment, ITag, IFile } from '../types';
|
import { IComment, IFile, INode, ITag, IValidationErrors } from '../types';
|
||||||
import { NODE_ACTIONS, NODE_TYPES } from './constants';
|
import { NODE_ACTIONS } from './constants';
|
||||||
import { INodeState } from './reducer';
|
import { INodeState } from './reducer';
|
||||||
|
|
||||||
export const nodeSet = (node: Partial<INodeState>) => ({
|
export const nodeSet = (node: Partial<INodeState>) => ({
|
||||||
|
@ -44,9 +44,14 @@ export const nodeSetCurrent = (current: INodeState['current']) => ({
|
||||||
type: NODE_ACTIONS.SET_CURRENT,
|
type: NODE_ACTIONS.SET_CURRENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const nodePostComment = (id: number, is_before: boolean) => ({
|
export const nodePostLocalComment = (
|
||||||
id,
|
nodeId: INode['id'],
|
||||||
is_before,
|
comment: IComment,
|
||||||
|
callback: (e?: string) => void
|
||||||
|
) => ({
|
||||||
|
nodeId,
|
||||||
|
comment,
|
||||||
|
callback,
|
||||||
type: NODE_ACTIONS.POST_COMMENT,
|
type: NODE_ACTIONS.POST_COMMENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -41,7 +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`,
|
||||||
SET_COMMENTS: `${prefix}SET_COMMENTS`,
|
SET_COMMENTS: `${prefix}SET_COMMENTS`,
|
||||||
SET_RELATED: `${prefix}SET_RELATED`,
|
SET_RELATED: `${prefix}SET_RELATED`,
|
||||||
|
|
||||||
|
@ -106,10 +106,7 @@ export const EMPTY_COMMENT: IComment = {
|
||||||
id: null,
|
id: null,
|
||||||
text: '',
|
text: '',
|
||||||
files: [],
|
files: [],
|
||||||
temp_ids: [],
|
|
||||||
is_private: false,
|
|
||||||
user: null,
|
user: null,
|
||||||
error: '',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NODE_EDITORS = {
|
export const NODE_EDITORS = {
|
||||||
|
|
|
@ -1,59 +1,52 @@
|
||||||
import { takeLatest, call, put, select, delay, all, takeLeading } from 'redux-saga/effects';
|
import { all, call, delay, put, select, takeLatest, takeLeading } from 'redux-saga/effects';
|
||||||
import { push } from 'connected-react-router';
|
import { push } from 'connected-react-router';
|
||||||
import { omit } from 'ramda';
|
import { omit } from 'ramda';
|
||||||
|
|
||||||
|
import { COMMENTS_DISPLAY, EMPTY_COMMENT, EMPTY_NODE, NODE_ACTIONS, NODE_EDITOR_DATA } from './constants';
|
||||||
import {
|
import {
|
||||||
NODE_ACTIONS,
|
nodeCancelCommentEdit,
|
||||||
EMPTY_NODE,
|
|
||||||
EMPTY_COMMENT,
|
|
||||||
NODE_EDITOR_DATA,
|
|
||||||
COMMENTS_DISPLAY,
|
|
||||||
} from './constants';
|
|
||||||
import {
|
|
||||||
nodeSave,
|
|
||||||
nodeSetSaveErrors,
|
|
||||||
nodeLoadNode,
|
|
||||||
nodeSetLoading,
|
|
||||||
nodeSetCurrent,
|
|
||||||
nodeSetLoadingComments,
|
|
||||||
nodePostComment,
|
|
||||||
nodeSetSendingComment,
|
|
||||||
nodeSetComments,
|
|
||||||
nodeSetCommentData,
|
|
||||||
nodeUpdateTags,
|
|
||||||
nodeSetTags,
|
|
||||||
nodeCreate,
|
nodeCreate,
|
||||||
nodeSetEditor,
|
|
||||||
nodeEdit,
|
nodeEdit,
|
||||||
nodeLike,
|
nodeEditComment,
|
||||||
nodeSetRelated,
|
|
||||||
nodeGotoNode,
|
nodeGotoNode,
|
||||||
|
nodeLike,
|
||||||
|
nodeLoadNode,
|
||||||
nodeLock,
|
nodeLock,
|
||||||
nodeLockComment,
|
nodeLockComment,
|
||||||
nodeEditComment,
|
nodePostLocalComment,
|
||||||
|
nodeSave,
|
||||||
nodeSet,
|
nodeSet,
|
||||||
nodeCancelCommentEdit,
|
nodeSetCommentData,
|
||||||
|
nodeSetComments,
|
||||||
|
nodeSetCurrent,
|
||||||
|
nodeSetEditor,
|
||||||
|
nodeSetLoading,
|
||||||
|
nodeSetLoadingComments,
|
||||||
|
nodeSetRelated,
|
||||||
|
nodeSetSaveErrors,
|
||||||
|
nodeSetTags,
|
||||||
|
nodeUpdateTags,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import {
|
import {
|
||||||
postNode,
|
|
||||||
getNode,
|
getNode,
|
||||||
postNodeComment,
|
|
||||||
getNodeComments,
|
getNodeComments,
|
||||||
updateNodeTags,
|
|
||||||
postNodeLike,
|
|
||||||
postNodeStar,
|
|
||||||
getNodeRelated,
|
getNodeRelated,
|
||||||
|
postNode,
|
||||||
|
postNodeComment,
|
||||||
|
postNodeLike,
|
||||||
postNodeLock,
|
postNodeLock,
|
||||||
postNodeLockComment,
|
postNodeLockComment,
|
||||||
|
postNodeStar,
|
||||||
|
updateNodeTags,
|
||||||
} from './api';
|
} from './api';
|
||||||
import { reqWrapper } from '../auth/sagas';
|
import { reqWrapper } from '../auth/sagas';
|
||||||
import { flowSetNodes, flowSetUpdated } from '../flow/actions';
|
import { flowSetNodes, flowSetUpdated } from '../flow/actions';
|
||||||
import { ERRORS } from '~/constants/errors';
|
import { ERRORS } from '~/constants/errors';
|
||||||
import { modalSetShown, modalShowDialog } from '../modal/actions';
|
import { modalSetShown, modalShowDialog } from '../modal/actions';
|
||||||
import { selectFlowNodes, selectFlow } from '../flow/selectors';
|
import { selectFlow, selectFlowNodes } from '../flow/selectors';
|
||||||
import { URLS } from '~/constants/urls';
|
import { URLS } from '~/constants/urls';
|
||||||
import { selectNode } from './selectors';
|
import { selectNode } from './selectors';
|
||||||
import { IResultWithStatus, INode, Unwrap } from '../types';
|
import { INode, IResultWithStatus, Unwrap } from '../types';
|
||||||
import { NODE_EDITOR_DIALOGS } from '~/constants/dialogs';
|
import { NODE_EDITOR_DIALOGS } from '~/constants/dialogs';
|
||||||
import { DIALOGS } from '~/redux/modal/constants';
|
import { DIALOGS } from '~/redux/modal/constants';
|
||||||
import { INodeState } from './reducer';
|
import { INodeState } from './reducer';
|
||||||
|
@ -196,36 +189,36 @@ 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);
|
const { data, error }: Unwrap<ReturnType<typeof postNodeComment>> = yield call(
|
||||||
|
reqWrapper,
|
||||||
|
postNodeComment,
|
||||||
|
{
|
||||||
|
data: comment,
|
||||||
|
id: nodeId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
yield put(nodeSetSendingComment(true));
|
if (error || !data.comment) {
|
||||||
const {
|
return callback(error);
|
||||||
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);
|
const { current }: ReturnType<typeof selectNode> = yield select(selectNode);
|
||||||
|
|
||||||
if (current_node && current_node.id === current.id) {
|
if (current?.id === nodeId) {
|
||||||
const { comments, comment_data: current_comment_data } = yield select(selectNode);
|
const { comments } = yield select(selectNode);
|
||||||
|
|
||||||
if (id === 0) {
|
if (!comment.id) {
|
||||||
yield put(nodeSetCommentData(0, { ...EMPTY_COMMENT }));
|
yield put(nodeSetComments([data.comment, ...comments]));
|
||||||
yield put(nodeSetComments([comment, ...comments]));
|
|
||||||
} else {
|
} else {
|
||||||
yield put(
|
yield put(
|
||||||
nodeSet({
|
nodeSet({
|
||||||
comment_data: omit([id.toString()], current_comment_data),
|
comments: comments.map(item => (item.id === comment.id ? data.comment : item)),
|
||||||
comments: comments.map(item => (item.id === id ? comment : item)),
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
callback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
|
||||||
// })
|
|
||||||
|
|
|
@ -142,11 +142,8 @@ export interface INode {
|
||||||
export interface IComment {
|
export interface IComment {
|
||||||
id: number;
|
id: number;
|
||||||
text: string;
|
text: string;
|
||||||
temp_ids?: string[];
|
|
||||||
files: IFile[];
|
files: IFile[];
|
||||||
is_private: boolean;
|
|
||||||
user: IUser;
|
user: IUser;
|
||||||
error?: string;
|
|
||||||
|
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
update_at?: string;
|
update_at?: string;
|
||||||
|
|
|
@ -230,6 +230,7 @@
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-height: 60vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status,
|
.status,
|
||||||
|
|
78
src/utils/hooks/fileUploader.tsx
Normal file
78
src/utils/hooks/fileUploader.tsx
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { IFile, IFileWithUUID } from '~/redux/types';
|
||||||
|
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
|
||||||
|
import { getFileType } from '~/utils/uploader';
|
||||||
|
import uuid from 'uuid4';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { uploadUploadFiles } from '~/redux/uploads/actions';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import { selectUploads } from '~/redux/uploads/selectors';
|
||||||
|
|
||||||
|
export const useFileUploader = (
|
||||||
|
subject: typeof UPLOAD_SUBJECTS[keyof typeof UPLOAD_SUBJECTS],
|
||||||
|
target: typeof UPLOAD_TARGETS[keyof typeof UPLOAD_TARGETS],
|
||||||
|
initialFiles?: IFile[]
|
||||||
|
) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { files: uploadedFiles, statuses } = useShallowSelect(selectUploads);
|
||||||
|
|
||||||
|
const [files, setFiles] = useState<IFile[]>(initialFiles || []);
|
||||||
|
const [pendingIDs, setPendingIDs] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const uploadFiles = useCallback(
|
||||||
|
(files: File[]) => {
|
||||||
|
const items: IFileWithUUID[] = files.map(
|
||||||
|
(file: File): IFileWithUUID => ({
|
||||||
|
file,
|
||||||
|
temp_id: uuid(),
|
||||||
|
subject,
|
||||||
|
target,
|
||||||
|
type: getFileType(file),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const temps = items.map(file => file.temp_id);
|
||||||
|
|
||||||
|
setPendingIDs([...pendingIDs, ...temps]);
|
||||||
|
dispatch(uploadUploadFiles(items));
|
||||||
|
},
|
||||||
|
[pendingIDs, setPendingIDs, dispatch, subject, target]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const added = pendingIDs
|
||||||
|
.map(temp_uuid => statuses[temp_uuid] && statuses[temp_uuid].uuid)
|
||||||
|
.map(el => !!el && uploadedFiles[el])
|
||||||
|
.filter(el => !!el && !files.some(file => file && file.id === el.id));
|
||||||
|
|
||||||
|
const newPending = pendingIDs.filter(
|
||||||
|
temp_id =>
|
||||||
|
statuses[temp_id] &&
|
||||||
|
(!statuses[temp_id].uuid || !added.some(file => file.id === statuses[temp_id].uuid))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (added.length) {
|
||||||
|
setPendingIDs(newPending);
|
||||||
|
setFiles([...files, ...added]);
|
||||||
|
}
|
||||||
|
}, [statuses, files, pendingIDs, uploadedFiles]);
|
||||||
|
|
||||||
|
const pending = useMemo(() => pendingIDs.map(id => statuses[id]).filter(el => !!el), [
|
||||||
|
statuses,
|
||||||
|
pendingIDs,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isLoading = pending.length > 0;
|
||||||
|
|
||||||
|
return { uploadFiles, pending, files, setFiles, isUploading: isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FileUploader = ReturnType<typeof useFileUploader>;
|
||||||
|
const FileUploaderContext = createContext<FileUploader>(null);
|
||||||
|
|
||||||
|
export const FileUploaderProvider: FC<{ value: FileUploader; children }> = ({
|
||||||
|
value,
|
||||||
|
children,
|
||||||
|
}) => <FileUploaderContext.Provider value={value}>{children}</FileUploaderContext.Provider>;
|
||||||
|
|
||||||
|
export const useFileUploaderContext = () => useContext(FileUploaderContext);
|
79
src/utils/hooks/useCommentFormFormik.ts
Normal file
79
src/utils/hooks/useCommentFormFormik.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import { IComment, INode } from '~/redux/types';
|
||||||
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import { FormikHelpers, useFormik, useFormikContext } from 'formik';
|
||||||
|
import { array, object, string } from 'yup';
|
||||||
|
import { FileUploader } from '~/utils/hooks/fileUploader';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { nodePostLocalComment } from '~/redux/node/actions';
|
||||||
|
|
||||||
|
const validationSchema = object().shape({
|
||||||
|
text: string(),
|
||||||
|
files: array(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSuccess = ({ resetForm, setStatus, setSubmitting }: FormikHelpers<IComment>) => (
|
||||||
|
e: string
|
||||||
|
) => {
|
||||||
|
setSubmitting(false);
|
||||||
|
|
||||||
|
if (e) {
|
||||||
|
setStatus(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetForm) {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCommentFormFormik = (
|
||||||
|
values: IComment,
|
||||||
|
nodeId: INode['id'],
|
||||||
|
uploader: FileUploader,
|
||||||
|
stopEditing?: () => void
|
||||||
|
) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { current: initialValues } = useRef(values);
|
||||||
|
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
(values: IComment, helpers: FormikHelpers<IComment>) => {
|
||||||
|
helpers.setSubmitting(true);
|
||||||
|
dispatch(
|
||||||
|
nodePostLocalComment(
|
||||||
|
nodeId,
|
||||||
|
{
|
||||||
|
...values,
|
||||||
|
files: uploader.files,
|
||||||
|
},
|
||||||
|
onSuccess(helpers)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch, nodeId, uploader.files]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onReset = useCallback(() => {
|
||||||
|
uploader.setFiles([]);
|
||||||
|
|
||||||
|
if (stopEditing) stopEditing();
|
||||||
|
}, [uploader, stopEditing]);
|
||||||
|
|
||||||
|
const formik = useFormik({
|
||||||
|
initialValues,
|
||||||
|
validationSchema,
|
||||||
|
onSubmit,
|
||||||
|
initialStatus: '',
|
||||||
|
onReset,
|
||||||
|
validateOnChange: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (formik.status) {
|
||||||
|
formik.setStatus('');
|
||||||
|
}
|
||||||
|
}, [formik.values.text]);
|
||||||
|
|
||||||
|
return formik;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCommentFormContext = () => useFormikContext<IComment>();
|
5
src/utils/hooks/useShallowSelect.ts
Normal file
5
src/utils/hooks/useShallowSelect.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { shallowEqual, useSelector } from 'react-redux';
|
||||||
|
import { IState } from '~/redux/store';
|
||||||
|
|
||||||
|
export const useShallowSelect = <T extends (state: IState) => any>(selector: T): ReturnType<T> =>
|
||||||
|
useSelector(selector, shallowEqual);
|
75
yarn.lock
75
yarn.lock
|
@ -1109,6 +1109,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.10.5":
|
||||||
|
version "7.13.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.7.tgz#d494e39d198ee9ca04f4dcb76d25d9d7a1dc961a"
|
||||||
|
integrity sha512-h+ilqoX998mRVM5FtB5ijRuHUDVt5l3yfoOi2uh18Z/O3hvyaHQ39NpxVkCIG5yFs+mLq/ewFp8Bss6zmWv6ZA==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/template@^7.10.4", "@babel/template@^7.4.0", "@babel/template@^7.8.6":
|
"@babel/template@^7.10.4", "@babel/template@^7.4.0", "@babel/template@^7.8.6":
|
||||||
version "7.10.4"
|
version "7.10.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
|
||||||
|
@ -1684,6 +1691,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
|
||||||
integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
|
integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
|
||||||
|
|
||||||
|
"@types/lodash@^4.14.165":
|
||||||
|
version "4.14.168"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008"
|
||||||
|
integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==
|
||||||
|
|
||||||
"@types/marked@^1.2.2":
|
"@types/marked@^1.2.2":
|
||||||
version "1.2.2"
|
version "1.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-1.2.2.tgz#1f858a0e690247ecf3b2eef576f98f86e8d960d4"
|
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-1.2.2.tgz#1f858a0e690247ecf3b2eef576f98f86e8d960d4"
|
||||||
|
@ -1775,6 +1787,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/yargs-parser" "*"
|
"@types/yargs-parser" "*"
|
||||||
|
|
||||||
|
"@types/yup@^0.29.11":
|
||||||
|
version "0.29.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.11.tgz#d654a112973f5e004bf8438122bd7e56a8e5cd7e"
|
||||||
|
integrity sha512-9cwk3c87qQKZrT251EDoibiYRILjCmxBvvcb4meofCmx1vdnNcR9gyildy5vOHASpOKMsn42CugxUvcwK5eu1g==
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin@^2.10.0":
|
"@typescript-eslint/eslint-plugin@^2.10.0":
|
||||||
version "2.34.0"
|
version "2.34.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9"
|
||||||
|
@ -3877,6 +3894,11 @@ deep-is@~0.1.3:
|
||||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
|
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
|
||||||
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
|
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
|
||||||
|
|
||||||
|
deepmerge@^2.1.1:
|
||||||
|
version "2.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
|
||||||
|
integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
|
||||||
|
|
||||||
default-gateway@^4.2.0:
|
default-gateway@^4.2.0:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b"
|
resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b"
|
||||||
|
@ -4998,6 +5020,19 @@ form-data@~2.3.2:
|
||||||
combined-stream "^1.0.6"
|
combined-stream "^1.0.6"
|
||||||
mime-types "^2.1.12"
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
|
formik@^2.2.6:
|
||||||
|
version "2.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.6.tgz#378a4bafe4b95caf6acf6db01f81f3fe5147559d"
|
||||||
|
integrity sha512-Kxk2zQRafy56zhLmrzcbryUpMBvT0tal5IvcifK5+4YNGelKsnrODFJ0sZQRMQboblWNym4lAW3bt+tf2vApSA==
|
||||||
|
dependencies:
|
||||||
|
deepmerge "^2.1.1"
|
||||||
|
hoist-non-react-statics "^3.3.0"
|
||||||
|
lodash "^4.17.14"
|
||||||
|
lodash-es "^4.17.14"
|
||||||
|
react-fast-compare "^2.0.1"
|
||||||
|
tiny-warning "^1.0.2"
|
||||||
|
tslib "^1.10.0"
|
||||||
|
|
||||||
forwarded@~0.1.2:
|
forwarded@~0.1.2:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
|
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
|
||||||
|
@ -7001,6 +7036,11 @@ locate-path@^5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate "^4.1.0"
|
p-locate "^4.1.0"
|
||||||
|
|
||||||
|
lodash-es@^4.17.14, lodash-es@^4.17.15:
|
||||||
|
version "4.17.21"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||||
|
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||||
|
|
||||||
lodash._reinterpolate@^3.0.0:
|
lodash._reinterpolate@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
||||||
|
@ -7448,6 +7488,11 @@ nan@^2.12.1, nan@^2.13.2:
|
||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
|
||||||
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
|
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
|
||||||
|
|
||||||
|
nanoclone@^0.2.1:
|
||||||
|
version "0.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4"
|
||||||
|
integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==
|
||||||
|
|
||||||
nanomatch@^1.2.9:
|
nanomatch@^1.2.9:
|
||||||
version "1.2.13"
|
version "1.2.13"
|
||||||
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
|
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
|
||||||
|
@ -9034,6 +9079,11 @@ prop-types@^15.5.7, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
react-is "^16.8.1"
|
react-is "^16.8.1"
|
||||||
|
|
||||||
|
property-expr@^2.0.4:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910"
|
||||||
|
integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==
|
||||||
|
|
||||||
proxy-addr@~2.0.5:
|
proxy-addr@~2.0.5:
|
||||||
version "2.0.6"
|
version "2.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
|
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
|
||||||
|
@ -9250,6 +9300,11 @@ react-error-overlay@^6.0.7:
|
||||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.8.tgz#474ed11d04fc6bda3af643447d85e9127ed6b5de"
|
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.8.tgz#474ed11d04fc6bda3af643447d85e9127ed6b5de"
|
||||||
integrity sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw==
|
integrity sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw==
|
||||||
|
|
||||||
|
react-fast-compare@^2.0.1:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||||
|
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||||
|
|
||||||
react-fast-compare@^3.0.1:
|
react-fast-compare@^3.0.1:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
||||||
|
@ -10868,7 +10923,7 @@ tiny-invariant@^1.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
||||||
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
|
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
|
||||||
|
|
||||||
tiny-warning@^1.0.0, tiny-warning@^1.0.3:
|
tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
||||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||||
|
@ -10932,6 +10987,11 @@ toidentifier@1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
||||||
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
||||||
|
|
||||||
|
toposort@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
|
||||||
|
integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
|
||||||
|
|
||||||
tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@^2.5.0, tough-cookie@~2.5.0:
|
tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@^2.5.0, tough-cookie@~2.5.0:
|
||||||
version "2.5.0"
|
version "2.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
|
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
|
||||||
|
@ -11820,3 +11880,16 @@ yargs@^13.3.0, yargs@^13.3.2:
|
||||||
which-module "^2.0.0"
|
which-module "^2.0.0"
|
||||||
y18n "^4.0.0"
|
y18n "^4.0.0"
|
||||||
yargs-parser "^13.1.2"
|
yargs-parser "^13.1.2"
|
||||||
|
|
||||||
|
yup@^0.32.9:
|
||||||
|
version "0.32.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.9.tgz#9367bec6b1b0e39211ecbca598702e106019d872"
|
||||||
|
integrity sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.10.5"
|
||||||
|
"@types/lodash" "^4.14.165"
|
||||||
|
lodash "^4.17.20"
|
||||||
|
lodash-es "^4.17.15"
|
||||||
|
nanoclone "^0.2.1"
|
||||||
|
property-expr "^2.0.4"
|
||||||
|
toposort "^2.0.2"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue