1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-05-02 08:06: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,31 +1,35 @@
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 { current } = useShallowSelect(selectNode);
const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]);
const stopEditing = useCallback(() => setIsEditing(false), [setIsEditing]);
const groupped = useMemo<Record<keyof typeof UPLOAD_TYPES, IFile[]>>( const groupped = useMemo<Record<keyof typeof UPLOAD_TYPES, IFile[]>>(
() => () =>
reduce( reduce(
@ -40,13 +44,9 @@ const CommentContent: FC<IProps> = memo(
onDelete(comment.id, !comment.deleted_at); onDelete(comment.id, !comment.deleted_at);
}, [comment, onDelete]); }, [comment, onDelete]);
const onEditClick = useCallback(() => {
onEdit(comment.id);
}, [comment, onEdit]);
const menu = useMemo( const menu = useMemo(
() => can_edit && <CommentMenu onDelete={onLockClick} onEdit={onEditClick} />, () => can_edit && <CommentMenu onDelete={onLockClick} onEdit={startEditing} />,
[can_edit, comment, onEditClick, onLockClick] [can_edit, startEditing, onLockClick]
); );
const blocks = useMemo( const blocks = useMemo(
@ -54,9 +54,13 @@ const CommentContent: FC<IProps> = memo(
!!comment.text.trim() !!comment.text.trim()
? formatCommentText(path(['user', 'username'], comment), comment.text) ? formatCommentText(path(['user', 'username'], comment), comment.text)
: [], : [],
[comment.text] [comment]
); );
if (isEditing) {
return <CommentForm nodeId={current.id} comment={comment} onCancelEdit={stopEditing} />;
}
return ( return (
<div className={styles.wrap}> <div className={styles.wrap}>
{comment.text && ( {comment.text && (
@ -106,7 +110,6 @@ const CommentContent: FC<IProps> = memo(
)} )}
</div> </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,
nodeCancelCommentEdit: NODE_ACTIONS.nodeCancelCommentEdit,
nodeSetCommentData: NODE_ACTIONS.nodeSetCommentData,
uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
};
type IProps = ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps & {
id: number;
is_before?: boolean;
};
const CommentFormUnconnected: FC<IProps> = memo(
({
node: { comment_data, is_sending_comment },
uploads: { statuses, files },
id,
is_before = false,
nodePostComment,
nodeSetCommentData,
uploadUploadFiles,
nodeCancelCommentEdit,
}) => {
const [textarea, setTextarea] = useState<HTMLTextAreaElement>(); const [textarea, setTextarea] = useState<HTMLTextAreaElement>();
const comment = useMemo(() => comment_data[id], [comment_data, id]); const uploader = useFileUploader(
UPLOAD_SUBJECTS.COMMENT,
const onUpload = useCallback( UPLOAD_TARGETS.COMMENTS,
(files: File[]) => { comment?.files
const items: IFileWithUUID[] = files.map(
(file: File): IFileWithUUID => ({
file,
temp_id: uuid(),
subject: UPLOAD_SUBJECTS.COMMENT,
target: UPLOAD_TARGETS.COMMENTS,
type: getFileType(file),
})
); );
const formik = useCommentFormFormik(comment || EMPTY_COMMENT, nodeId, uploader, onCancelEdit);
const isLoading = formik.isSubmitting || uploader.isUploading;
const isEditing = !!comment?.id;
const temps = items.map(file => file.temp_id); const clearError = useCallback(() => {
if (formik.status) {
formik.setStatus('');
}
nodeSetCommentData(id, assocPath(['temp_ids'], [...comment.temp_ids, ...temps], comment)); if (formik.errors.text) {
uploadUploadFiles(items); formik.setErrors({
}, ...formik.errors,
[uploadUploadFiles, comment, id, nodeSetCommentData] text: '',
);
const onInput = useCallback<InputHandler>(
text => {
nodeSetCommentData(id, assocPath(['text'], text, comment));
},
[nodeSetCommentData, comment, id]
);
useEffect(() => {
const temp_ids = (comment && comment.temp_ids) || [];
const added_files = temp_ids
.map(temp_uuid => statuses[temp_uuid] && statuses[temp_uuid].uuid)
.map(el => !!el && files[el])
.filter(el => !!el && !comment.files.some(file => file && file.id === el.id));
const filtered_temps = temp_ids.filter(
temp_id =>
statuses[temp_id] &&
(!statuses[temp_id].uuid || !added_files.some(file => file.id === statuses[temp_id].uuid))
);
if (added_files.length) {
nodeSetCommentData(id, {
...comment,
temp_ids: filtered_temps,
files: [...comment.files, ...added_files],
}); });
} }
}, [statuses, files]); }, [formik]);
const isUploadingNow = useMemo(() => comment.temp_ids.length > 0, [comment.temp_ids]); const error = formik.status || formik.errors.text;
const onSubmit = useCallback(
event => {
if (event) event.preventDefault();
if (isUploadingNow || is_sending_comment) return;
nodePostComment(id, is_before);
},
[nodePostComment, id, is_before, isUploadingNow, is_sending_comment]
);
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
({ ctrlKey, key }) => {
if (!!ctrlKey && key === 'Enter') onSubmit(null);
},
[onSubmit]
);
const images = useMemo(
() => comment.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE),
[comment.files]
);
const locked_images = useMemo(
() =>
comment.temp_ids
.filter(temp => statuses[temp] && statuses[temp].type === UPLOAD_TYPES.IMAGE)
.map(temp_id => statuses[temp_id]),
[statuses, comment.temp_ids]
);
const audios = useMemo(
() => comment.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO),
[comment.files]
);
const locked_audios = useMemo(
() =>
comment.temp_ids
.filter(temp => statuses[temp] && statuses[temp].type === UPLOAD_TYPES.AUDIO)
.map(temp_id => statuses[temp_id]),
[statuses, comment.temp_ids]
);
const onCancelEdit = useCallback(() => {
nodeCancelCommentEdit(id);
}, [nodeCancelCommentEdit, comment.id]);
const placeholder = useRandomPhrase('SIMPLE');
const clearError = useCallback(() => nodeSetCommentData(id, { error: '' }), [
id,
nodeSetCommentData,
]);
useEffect(() => {
if (comment.error) clearError();
}, [comment.files, comment.text]);
const setData = useCallback(
(data: Partial<IComment>) => {
nodeSetCommentData(id, data);
},
[nodeSetCommentData, id]
);
return ( return (
<CommentFormDropzone onUpload={onUpload}> <CommentFormDropzone onUpload={uploader.uploadFiles}>
<form onSubmit={onSubmit} className={styles.wrap}> <form onSubmit={formik.handleSubmit} className={styles.wrap}>
<FormikProvider value={formik}>
<FileUploaderProvider value={uploader}>
<div className={styles.input}> <div className={styles.input}>
<Textarea <LocalCommentFormTextarea setRef={setTextarea} />
value={comment.text}
handler={onInput}
onKeyDown={onKeyDown}
disabled={is_sending_comment}
placeholder={placeholder}
minRows={2}
setRef={setTextarea}
/>
{comment.error && ( {!!error && (
<div className={styles.error} onClick={clearError}> <div className={styles.error} onClick={clearError}>
{ERROR_LITERAL[comment.error] || comment.error} {ERROR_LITERAL[error] || error}
</div> </div>
)} )}
</div> </div>
<CommentFormAttaches <CommentFormAttaches />
images={images}
audios={audios}
locked_audios={locked_audios}
locked_images={locked_images}
comment={comment}
setComment={setData}
onUpload={onUpload}
/>
<Group horizontal className={styles.buttons}> <Group horizontal className={styles.buttons}>
<CommentFormAttachButtons onUpload={onUpload} /> <CommentFormAttachButtons onUpload={uploader.uploadFiles} />
<CommentFormFormatButtons element={textarea} handler={onInput} /> <CommentFormFormatButtons element={textarea} handler={formik.handleChange('text')} />
<Filler /> {isLoading && <LoaderCircle size={20} />}
{(is_sending_comment || isUploadingNow) && <LoaderCircle size={20} />} {isEditing && (
{id !== 0 && (
<Button size="small" color="link" type="button" onClick={onCancelEdit}> <Button size="small" color="link" type="button" onClick={onCancelEdit}>
Отмена Отмена
</Button> </Button>
)} )}
<Button <Button
type="submit"
size="small" size="small"
color="gray" color="gray"
iconRight={id === 0 ? 'enter' : 'check'} iconRight={!isEditing ? 'enter' : 'check'}
disabled={is_sending_comment || isUploadingNow} disabled={isLoading}
> >
{id === 0 ? 'Сказать' : 'Сохранить'} {!isEditing ? 'Сказать' : 'Сохранить'}
</Button> </Button>
</Group> </Group>
</FileUploaderProvider>
</FormikProvider>
</form> </form>
</CommentFormDropzone> </CommentFormDropzone>
); );
} };
);
const CommentForm = connect(mapStateToProps, mapDispatchToProps)(CommentFormUnconnected); export { CommentForm };
export { CommentForm, CommentFormUnconnected };

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,7 @@
import React, { FC, useEffect } from 'react'; import React, { FC, useEffect } from 'react';
import { RouteComponentProps } from 'react-router'; import { selectNode, selectNodeComments } from '~/redux/node/selectors';
import { selectNode } from '~/redux/node/selectors';
import { selectUser } from '~/redux/auth/selectors'; import { selectUser } from '~/redux/auth/selectors';
import { connect } from 'react-redux'; import { useDispatch } from 'react-redux';
import { NodeComments } from '~/components/node/NodeComments'; import { NodeComments } from '~/components/node/NodeComments';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { Group } from '~/components/containers/Group'; import { Group } from '~/components/containers/Group';
@ -10,72 +9,49 @@ import boris from '~/sprites/boris_robot.svg';
import { NodeNoComments } from '~/components/node/NodeNoComments'; import { NodeNoComments } from '~/components/node/NodeNoComments';
import { useRandomPhrase } from '~/constants/phrases'; import { useRandomPhrase } from '~/constants/phrases';
import { NodeCommentForm } from '~/components/node/NodeCommentForm'; import { NodeCommentForm } from '~/components/node/NodeCommentForm';
import * as NODE_ACTIONS from '~/redux/node/actions';
import * as AUTH_ACTIONS from '~/redux/auth/actions';
import * as MODAL_ACTIONS from '~/redux/modal/actions';
import * as BORIS_ACTIONS from '~/redux/boris/actions';
import isBefore from 'date-fns/isBefore'; import isBefore from 'date-fns/isBefore';
import { Card } from '~/components/containers/Card'; import { Card } from '~/components/containers/Card';
import { Footer } from '~/components/main/Footer'; import { Footer } from '~/components/main/Footer';
import { Sticky } from '~/components/containers/Sticky'; import { Sticky } from '~/components/containers/Sticky';
import { selectBorisStats } from '~/redux/boris/selectors';
import { BorisStats } from '~/components/boris/BorisStats'; import { BorisStats } from '~/components/boris/BorisStats';
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
import { selectBorisStats } from '~/redux/boris/selectors';
import { authSetUser } from '~/redux/auth/actions';
import { nodeLoadNode } from '~/redux/node/actions';
import { borisLoadStats } from '~/redux/boris/actions';
const mapStateToProps = state => ({ type IProps = {};
node: selectNode(state),
user: selectUser(state),
stats: selectBorisStats(state),
});
const mapDispatchToProps = { const BorisLayout: FC<IProps> = () => {
nodeLoadNode: NODE_ACTIONS.nodeLoadNode,
nodeLockComment: NODE_ACTIONS.nodeLockComment,
nodeEditComment: NODE_ACTIONS.nodeEditComment,
nodeLoadMoreComments: NODE_ACTIONS.nodeLoadMoreComments,
authSetUser: AUTH_ACTIONS.authSetUser,
modalShowPhotoswipe: MODAL_ACTIONS.modalShowPhotoswipe,
borisLoadStats: BORIS_ACTIONS.borisLoadStats,
};
type IProps = ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps &
RouteComponentProps<{ id: string }> & {};
const id = 696;
const BorisLayoutUnconnected: FC<IProps> = ({
node: { is_loading, is_loading_comments, comments = [], comment_data, comment_count },
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"