1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-05-03 00:26:41 +07:00

Merge branch 'develop'

This commit is contained in:
Fedor Katurov 2021-02-27 19:05:27 +07:00
commit 5d3e598a02
27 changed files with 698 additions and 674 deletions
package.json
src
components
comment
Comment
CommentContent
CommentForm
CommentFormAttaches
CommentFormFormatButtons
LocalCommentFormTextarea
input
node
CommendDeleted
NodeCommentForm
NodeComments
containers
node
BorisLayout
NodeLayout
profile/ProfileLayout
redux
styles/common
utils/hooks
yarn.lock

View file

@ -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"

View file

@ -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}
/> />
); );

View file

@ -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 };

View file

@ -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 };

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( ...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}
/> />

View file

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

View file

@ -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;

View 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 };

View file

@ -178,17 +178,6 @@
fill: white; fill: white;
} }
} }
> * {
margin: 0 5px;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
} }
.micro { .micro {

View file

@ -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}

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

@ -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>
); );
}; };

View file

@ -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 };

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 },
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 };

View file

@ -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}>

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

@ -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,
}); });

View file

@ -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 = {

View file

@ -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();
} }
} }

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,
// })

View file

@ -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;

View file

@ -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,

View 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);

View 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>();

View 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);

View file

@ -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"