mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
removed almost all node sagas
This commit is contained in:
parent
f76a5a4798
commit
168ba8cc04
30 changed files with 268 additions and 448 deletions
|
@ -14,6 +14,7 @@ type IProps = HTMLAttributes<HTMLDivElement> & {
|
|||
group: ICommentGroup;
|
||||
isSame?: boolean;
|
||||
canEdit?: boolean;
|
||||
saveComment: (data: IComment) => Promise<unknown>;
|
||||
onDelete: (id: IComment['id'], isLocked: boolean) => void;
|
||||
onShowImageModal: (images: IFile[], index: number) => void;
|
||||
};
|
||||
|
@ -29,6 +30,7 @@ const Comment: FC<IProps> = memo(
|
|||
canEdit,
|
||||
onDelete,
|
||||
onShowImageModal,
|
||||
saveComment,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
|
@ -50,6 +52,7 @@ const Comment: FC<IProps> = memo(
|
|||
|
||||
return (
|
||||
<CommentContent
|
||||
saveComment={saveComment}
|
||||
nodeId={nodeId}
|
||||
comment={comment}
|
||||
key={comment.id}
|
||||
|
|
|
@ -18,12 +18,13 @@ interface IProps {
|
|||
nodeId: number;
|
||||
comment: IComment;
|
||||
canEdit: boolean;
|
||||
saveComment: (data: IComment) => Promise<unknown>;
|
||||
onDelete: (id: IComment['id'], isLocked: boolean) => void;
|
||||
onShowImageModal: (images: IFile[], index: number) => void;
|
||||
}
|
||||
|
||||
const CommentContent: FC<IProps> = memo(
|
||||
({ comment, canEdit, nodeId, onDelete, onShowImageModal }) => {
|
||||
({ comment, canEdit, nodeId, saveComment, onDelete, onShowImageModal }) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]);
|
||||
|
@ -58,7 +59,14 @@ const CommentContent: FC<IProps> = memo(
|
|||
);
|
||||
|
||||
if (isEditing) {
|
||||
return <CommentForm nodeId={nodeId} comment={comment} onCancelEdit={stopEditing} />;
|
||||
return (
|
||||
<CommentForm
|
||||
saveComment={saveComment}
|
||||
nodeId={nodeId}
|
||||
comment={comment}
|
||||
onCancelEdit={stopEditing}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -20,17 +20,24 @@ import { Filler } from '~/components/containers/Filler';
|
|||
interface IProps {
|
||||
comment?: IComment;
|
||||
nodeId: INode['id'];
|
||||
saveComment: (data: IComment) => Promise<unknown>;
|
||||
onCancelEdit?: () => void;
|
||||
}
|
||||
|
||||
const CommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
|
||||
const CommentForm: FC<IProps> = ({ comment, nodeId, saveComment, onCancelEdit }) => {
|
||||
const [textarea, setTextarea] = useState<HTMLTextAreaElement>();
|
||||
const uploader = useFileUploader(
|
||||
UPLOAD_SUBJECTS.COMMENT,
|
||||
UPLOAD_TARGETS.COMMENTS,
|
||||
comment?.files
|
||||
);
|
||||
const formik = useCommentFormFormik(comment || EMPTY_COMMENT, nodeId, uploader, onCancelEdit);
|
||||
const formik = useCommentFormFormik(
|
||||
comment || EMPTY_COMMENT,
|
||||
nodeId,
|
||||
uploader,
|
||||
saveComment,
|
||||
onCancelEdit
|
||||
);
|
||||
const isLoading = formik.isSubmitting || uploader.isUploading;
|
||||
const isEditing = !!comment?.id;
|
||||
|
||||
|
|
|
@ -1,27 +1,21 @@
|
|||
import React, { FC } from 'react';
|
||||
import { CommentWrapper } from '~/components/containers/CommentWrapper';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectAuthUser } from '~/redux/auth/selectors';
|
||||
import { CommentForm } from '~/components/comment/CommentForm';
|
||||
import { INode } from '~/redux/types';
|
||||
import { IComment } from '~/redux/types';
|
||||
import { IUser } from '~/redux/auth/types';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
user: selectAuthUser(state),
|
||||
});
|
||||
interface NodeCommentFormProps {
|
||||
user: IUser;
|
||||
nodeId?: number;
|
||||
saveComment: (comment: IComment) => Promise<unknown>;
|
||||
}
|
||||
|
||||
type IProps = ReturnType<typeof mapStateToProps> & {
|
||||
isBefore?: boolean;
|
||||
nodeId: INode['id'];
|
||||
};
|
||||
|
||||
const NodeCommentFormUnconnected: FC<IProps> = ({ user, isBefore, nodeId }) => {
|
||||
const NodeCommentForm: FC<NodeCommentFormProps> = ({ user, nodeId, saveComment }) => {
|
||||
return (
|
||||
<CommentWrapper user={user} isForm>
|
||||
<CommentForm nodeId={nodeId} />
|
||||
<CommentForm nodeId={nodeId} saveComment={saveComment} />
|
||||
</CommentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const NodeCommentForm = connect(mapStateToProps)(NodeCommentFormUnconnected);
|
||||
|
||||
export { NodeCommentForm };
|
||||
|
|
|
@ -27,7 +27,7 @@ export const API = {
|
|||
GET_DIFF: '/flow/diff',
|
||||
GET_NODE: (id: number | string) => `/node/${id}`,
|
||||
|
||||
COMMENT: (id: INode['id']) => `/node/${id}/comment`,
|
||||
COMMENT: (id: INode['id'] | string) => `/node/${id}/comment`,
|
||||
RELATED: (id: INode['id']) => `/node/${id}/related`,
|
||||
UPDATE_TAGS: (id: INode['id']) => `/node/${id}/tags`,
|
||||
DELETE_TAG: (id: INode['id'], tagId: ITag['ID']) => `/node/${id}/tags/${tagId}`,
|
||||
|
|
|
@ -16,24 +16,28 @@ const BorisComments: FC<IProps> = () => {
|
|||
const {
|
||||
isLoading,
|
||||
comments,
|
||||
onSaveComment,
|
||||
onLoadMoreComments,
|
||||
onDeleteComment,
|
||||
onShowImageModal,
|
||||
count,
|
||||
hasMore,
|
||||
} = useCommentContext();
|
||||
const { node } = useNodeContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group className={styles.grid}>
|
||||
{user.is_user && <NodeCommentForm isBefore nodeId={node.id} />}
|
||||
{user.is_user && (
|
||||
<NodeCommentForm user={user} nodeId={node.id} saveComment={onSaveComment} />
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<NodeNoComments is_loading count={7} />
|
||||
) : (
|
||||
<CommentContextProvider
|
||||
onSaveComment={onSaveComment}
|
||||
comments={comments}
|
||||
count={count}
|
||||
hasMore={hasMore}
|
||||
onDeleteComment={onDeleteComment}
|
||||
onLoadMoreComments={onLoadMoreComments}
|
||||
onShowImageModal={onShowImageModal}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useHistory, useRouteMatch } from 'react-router';
|
|||
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
|
||||
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
||||
import styles from './styles.module.scss';
|
||||
import { useGetNode } from '~/hooks/node/useGetNode';
|
||||
import { useLoadNode } from '~/hooks/node/useLoadNode';
|
||||
import { useUpdateNode } from '~/hooks/node/useUpdateNode';
|
||||
import { INode } from '~/redux/types';
|
||||
|
||||
|
@ -24,7 +24,7 @@ const EditorEditDialog: FC = () => {
|
|||
history.replace(backUrl);
|
||||
}, [backUrl, history]);
|
||||
|
||||
const { node, isLoading } = useGetNode(parseInt(id, 10));
|
||||
const { node, isLoading } = useLoadNode(parseInt(id, 10));
|
||||
const updateNode = useUpdateNode(parseInt(id, 10));
|
||||
|
||||
const onSubmit = useCallback(
|
||||
|
|
|
@ -21,9 +21,9 @@ interface IProps {
|
|||
}
|
||||
|
||||
const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => {
|
||||
const { is_user: isUser } = useUserContext();
|
||||
const user = useUserContext();
|
||||
const { node, isLoading } = useNodeContext();
|
||||
const { comments, isLoading: isLoadingComments } = useCommentContext();
|
||||
const { comments, isLoading: isLoadingComments, onSaveComment } = useCommentContext();
|
||||
const { related, isLoading: isLoadingRelated } = useNodeRelatedContext();
|
||||
const { inline } = useNodeBlocks(node, isLoading);
|
||||
|
||||
|
@ -44,7 +44,9 @@ const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => {
|
|||
<NodeComments order={commentsOrder} />
|
||||
)}
|
||||
|
||||
{isUser && !isLoading && <NodeCommentForm nodeId={node.id} />}
|
||||
{user.is_user && !isLoading && (
|
||||
<NodeCommentForm nodeId={node.id} saveComment={onSaveComment} user={user} />
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<div className={styles.panel}>
|
||||
|
|
|
@ -21,27 +21,24 @@ const NodeComments: FC<IProps> = memo(({ order }) => {
|
|||
|
||||
const {
|
||||
comments,
|
||||
count,
|
||||
hasMore,
|
||||
lastSeenCurrent,
|
||||
onLoadMoreComments,
|
||||
onDeleteComment,
|
||||
onShowImageModal,
|
||||
onSaveComment,
|
||||
} = useCommentContext();
|
||||
|
||||
const left = useMemo(() => Math.max(0, count - comments.length), [comments, count]);
|
||||
|
||||
const groupped: ICommentGroup[] = useGrouppedComments(comments, order, lastSeenCurrent);
|
||||
|
||||
const more = useMemo(
|
||||
() =>
|
||||
left > 0 && (
|
||||
hasMore && (
|
||||
<div className={styles.more} onClick={onLoadMoreComments}>
|
||||
Показать ещё{' '}
|
||||
{plural(Math.min(left, COMMENTS_DISPLAY), 'комментарий', 'комментария', 'комментариев')}
|
||||
{left > COMMENTS_DISPLAY ? ` из ${left} оставшихся` : ''}
|
||||
Показать ещё комментарии
|
||||
</div>
|
||||
),
|
||||
[left, onLoadMoreComments]
|
||||
[hasMore, onLoadMoreComments]
|
||||
);
|
||||
|
||||
if (!node?.id) {
|
||||
|
@ -61,6 +58,7 @@ const NodeComments: FC<IProps> = memo(({ order }) => {
|
|||
onDelete={onDeleteComment}
|
||||
onShowImageModal={onShowImageModal}
|
||||
isSame={group.user.id === user.id}
|
||||
saveComment={onSaveComment}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ const ProfileLayoutUnconnected: FC<IProps> = ({ history, nodeSetCoverImage }) =>
|
|||
|
||||
<Grid className={styles.content}>
|
||||
<div className={styles.comments}>
|
||||
<CommentForm nodeId={0} />
|
||||
<CommentForm nodeId={0} saveComment={async () => console.log()} />
|
||||
<NodeNoComments is_loading={false} />
|
||||
</div>
|
||||
</Grid>
|
||||
|
|
|
@ -3,21 +3,23 @@ import { useCallback, useEffect, useRef } from 'react';
|
|||
import { FormikHelpers, useFormik, useFormikContext } from 'formik';
|
||||
import { array, object, string } from 'yup';
|
||||
import { FileUploader } from '~/hooks/data/useFileUploader';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { nodePostLocalComment } from '~/redux/node/actions';
|
||||
import { showErrorToast } from '~/utils/errors/showToast';
|
||||
import { hasPath, path } from 'ramda';
|
||||
|
||||
const validationSchema = object().shape({
|
||||
text: string(),
|
||||
files: array(),
|
||||
});
|
||||
|
||||
const onSuccess = ({ resetForm, setStatus, setSubmitting }: FormikHelpers<IComment>) => (
|
||||
e?: string
|
||||
const onSuccess = ({ resetForm, setSubmitting, setErrors }: FormikHelpers<IComment>) => (
|
||||
error?: unknown
|
||||
) => {
|
||||
setSubmitting(false);
|
||||
|
||||
if (e) {
|
||||
setStatus(e);
|
||||
if (hasPath(['response', 'data', 'error'], error)) {
|
||||
const message = path(['response', 'data', 'error'], error) as string;
|
||||
setErrors({ text: message });
|
||||
showErrorToast(error);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -30,26 +32,23 @@ export const useCommentFormFormik = (
|
|||
values: IComment,
|
||||
nodeId: INode['id'],
|
||||
uploader: FileUploader,
|
||||
sendData: (data: IComment) => Promise<unknown>,
|
||||
stopEditing?: () => void
|
||||
) => {
|
||||
const dispatch = useDispatch();
|
||||
const { current: initialValues } = useRef(values);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(values: IComment, helpers: FormikHelpers<IComment>) => {
|
||||
async (values: IComment, helpers: FormikHelpers<IComment>) => {
|
||||
try {
|
||||
helpers.setSubmitting(true);
|
||||
dispatch(
|
||||
nodePostLocalComment(
|
||||
nodeId,
|
||||
{
|
||||
...values,
|
||||
files: uploader.files,
|
||||
await sendData(values);
|
||||
onSuccess(helpers)();
|
||||
} catch (error) {
|
||||
console.log('error', error);
|
||||
onSuccess(helpers)(error);
|
||||
}
|
||||
},
|
||||
onSuccess(helpers)
|
||||
)
|
||||
);
|
||||
},
|
||||
[dispatch, nodeId, uploader.files]
|
||||
[sendData]
|
||||
);
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
|
|
52
src/hooks/comments/useGetComments.ts
Normal file
52
src/hooks/comments/useGetComments.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { KeyLoader } from 'swr';
|
||||
import { IComment } from '~/redux/types';
|
||||
import { API } from '~/constants/api';
|
||||
import { flatten, isNil } from 'ramda';
|
||||
import useSWRInfinite from 'swr/infinite';
|
||||
import { apiGetNodeComments } from '~/redux/node/api';
|
||||
import { COMMENTS_DISPLAY } from '~/redux/node/constants';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const getKey: (nodeId: number) => KeyLoader<IComment[]> = (nodeId: number) => (
|
||||
pageIndex,
|
||||
previousPageData
|
||||
) => {
|
||||
if (pageIndex > 0 && !previousPageData?.length) return null;
|
||||
return `${API.NODE.COMMENT(nodeId)}?page=${pageIndex}`;
|
||||
};
|
||||
|
||||
const extractKey = (key: string) => {
|
||||
const re = new RegExp(`${API.NODE.COMMENT('\\d+')}\\?page=(\\d+)`);
|
||||
const match = key.match(re);
|
||||
|
||||
if (!match || !Array.isArray(match) || isNil(match[1])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return parseInt(match[1], 10) || 0;
|
||||
};
|
||||
|
||||
export const useGetComments = (nodeId: number) => {
|
||||
// TODO: const postedCommentsLength = Math.min(0, data[data.length - 1] - COMMENTS_DISPLAY);
|
||||
|
||||
const { data, isValidating, setSize, size, mutate } = useSWRInfinite(
|
||||
getKey(nodeId),
|
||||
async (key: string) => {
|
||||
const result = await apiGetNodeComments({
|
||||
id: nodeId,
|
||||
take: COMMENTS_DISPLAY,
|
||||
skip: extractKey(key) * COMMENTS_DISPLAY, // TODO: - postedCommentsLength,
|
||||
});
|
||||
|
||||
return result.comments;
|
||||
}
|
||||
);
|
||||
|
||||
const comments = flatten(data || []);
|
||||
const hasMore =
|
||||
!!data?.[data?.length - 1].length && data[data.length - 1].length === COMMENTS_DISPLAY;
|
||||
|
||||
const onLoadMoreComments = useCallback(() => setSize(size + 1), [setSize, size]);
|
||||
|
||||
return { comments, hasMore, onLoadMoreComments, isLoading: !data && isValidating, mutate, data };
|
||||
};
|
62
src/hooks/comments/useNodeComments.ts
Normal file
62
src/hooks/comments/useNodeComments.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { useCallback } from 'react';
|
||||
import { IComment } from '~/redux/types';
|
||||
import { useGetComments } from '~/hooks/comments/useGetComments';
|
||||
import { apiLockComment, apiPostComment } from '~/redux/node/api';
|
||||
import { showErrorToast } from '~/utils/errors/showToast';
|
||||
|
||||
export const useNodeComments = (nodeId: number) => {
|
||||
const { comments, isLoading, onLoadMoreComments, hasMore, data, mutate } = useGetComments(nodeId);
|
||||
|
||||
const onDelete = useCallback(
|
||||
async (id: IComment['id'], isLocked: boolean) => {
|
||||
try {
|
||||
const { deleted_at } = await apiLockComment({ id, nodeId, isLocked });
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
await mutate(
|
||||
prev =>
|
||||
prev?.map(list =>
|
||||
list.map(comment => (comment.id === id ? { ...comment, deleted_at } : comment))
|
||||
),
|
||||
false
|
||||
);
|
||||
} catch (error) {
|
||||
showErrorToast(error);
|
||||
}
|
||||
},
|
||||
[data, mutate, nodeId]
|
||||
);
|
||||
|
||||
const onEdit = useCallback(
|
||||
async (comment: IComment) => {
|
||||
const result = await apiPostComment({ id: nodeId, data: comment });
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Comment was created
|
||||
if (!comment.id) {
|
||||
await mutate(
|
||||
data.map((list, index) => (index === 0 ? [result.comment, ...list] : list)),
|
||||
false
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await mutate(
|
||||
prev =>
|
||||
prev?.map(list =>
|
||||
list.map(it => (it.id === result.comment.id ? { ...it, ...result.comment } : it))
|
||||
),
|
||||
false
|
||||
);
|
||||
},
|
||||
[data, mutate, nodeId]
|
||||
);
|
||||
|
||||
return { onLoadMoreComments, onDelete, comments, hasMore, isLoading, onEdit };
|
||||
};
|
|
@ -1,20 +0,0 @@
|
|||
import { useShallowSelect } from '~/hooks/data/useShallowSelect';
|
||||
import { selectNode } from '~/redux/node/selectors';
|
||||
import { useLoadNode } from '~/hooks/node/useLoadNode';
|
||||
import { useOnNodeSeen } from '~/hooks/node/useOnNodeSeen';
|
||||
|
||||
export const useFullNode = (id: string) => {
|
||||
const {
|
||||
is_loading: isLoading,
|
||||
current: node,
|
||||
comments,
|
||||
comment_count: commentsCount,
|
||||
is_loading_comments: isLoadingComments,
|
||||
lastSeenCurrent,
|
||||
} = useShallowSelect(selectNode);
|
||||
|
||||
useLoadNode(id);
|
||||
// useOnNodeSeen(node);
|
||||
|
||||
return { node, comments, commentsCount, lastSeenCurrent, isLoading, isLoadingComments };
|
||||
};
|
|
@ -1,30 +0,0 @@
|
|||
import useSWR from 'swr';
|
||||
import { ApiGetNodeResponse } from '~/redux/node/types';
|
||||
import { API } from '~/constants/api';
|
||||
import { useOnNodeSeen } from '~/hooks/node/useOnNodeSeen';
|
||||
import { apiGetNode } from '~/redux/node/api';
|
||||
import { useCallback } from 'react';
|
||||
import { INode } from '~/redux/types';
|
||||
import { EMPTY_NODE } from '~/redux/node/constants';
|
||||
|
||||
export const useGetNode = (id: number) => {
|
||||
const { data, isValidating, mutate } = useSWR<ApiGetNodeResponse>(API.NODE.GET_NODE(id), () =>
|
||||
apiGetNode({ id })
|
||||
);
|
||||
|
||||
const update = useCallback(
|
||||
async (node?: Partial<INode>) => {
|
||||
if (!data?.node) {
|
||||
await mutate();
|
||||
return;
|
||||
}
|
||||
|
||||
await mutate({ node: { ...data.node, ...node } }, true);
|
||||
},
|
||||
[data, mutate]
|
||||
);
|
||||
|
||||
useOnNodeSeen(data?.node);
|
||||
|
||||
return { node: data?.node || EMPTY_NODE, isLoading: isValidating && !data, update };
|
||||
};
|
|
@ -1,12 +1,35 @@
|
|||
import { useEffect } from 'react';
|
||||
import { nodeGotoNode } from '~/redux/node/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import useSWR from 'swr';
|
||||
import { ApiGetNodeResponse } from '~/redux/node/types';
|
||||
import { API } from '~/constants/api';
|
||||
import { useOnNodeSeen } from '~/hooks/node/useOnNodeSeen';
|
||||
import { apiGetNode } from '~/redux/node/api';
|
||||
import { useCallback } from 'react';
|
||||
import { INode } from '~/redux/types';
|
||||
import { EMPTY_NODE } from '~/redux/node/constants';
|
||||
|
||||
// useLoadNode loads node on id change
|
||||
export const useLoadNode = (id: any) => {
|
||||
const dispatch = useDispatch();
|
||||
export const useLoadNode = (id: number) => {
|
||||
const { data, isValidating, mutate } = useSWR<ApiGetNodeResponse>(API.NODE.GET_NODE(id), () =>
|
||||
apiGetNode({ id })
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(nodeGotoNode(parseInt(id, 10), undefined));
|
||||
}, [dispatch, id]);
|
||||
const update = useCallback(
|
||||
async (node?: Partial<INode>) => {
|
||||
if (!data?.node) {
|
||||
await mutate();
|
||||
return;
|
||||
}
|
||||
|
||||
await mutate({ node: { ...data.node, ...node } }, true);
|
||||
},
|
||||
[data, mutate]
|
||||
);
|
||||
|
||||
useOnNodeSeen(data?.node);
|
||||
|
||||
return {
|
||||
node: data?.node || EMPTY_NODE,
|
||||
isLoading: isValidating && !data,
|
||||
update,
|
||||
lastSeen: data?.last_seen,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import { useCallback } from 'react';
|
||||
import { nodeLoadMoreComments, nodeLockComment } from '~/redux/node/actions';
|
||||
import { IComment } from '~/redux/types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
export const useNodeComments = (nodeId: number) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onLoadMoreComments = useCallback(() => dispatch(nodeLoadMoreComments()), [dispatch]);
|
||||
|
||||
const onDelete = useCallback(
|
||||
(id: IComment['id'], locked: boolean) => dispatch(nodeLockComment(id, locked, nodeId)),
|
||||
[dispatch, nodeId]
|
||||
);
|
||||
|
||||
return { onLoadMoreComments, onDelete };
|
||||
};
|
|
@ -2,11 +2,11 @@ import { useHistory } from 'react-router';
|
|||
import { useCallback } from 'react';
|
||||
import { ITag } from '~/redux/types';
|
||||
import { URLS } from '~/constants/urls';
|
||||
import { useGetNode } from '~/hooks/node/useGetNode';
|
||||
import { useLoadNode } from '~/hooks/node/useLoadNode';
|
||||
import { apiDeleteNodeTag, apiPostNodeTags } from '~/redux/node/api';
|
||||
|
||||
export const useNodeTags = (id: number) => {
|
||||
const { update } = useGetNode(id);
|
||||
const { update } = useLoadNode(id);
|
||||
const history = useHistory();
|
||||
|
||||
const onChange = useCallback(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useGetNode } from '~/hooks/node/useGetNode';
|
||||
import { useLoadNode } from '~/hooks/node/useLoadNode';
|
||||
import { useCallback } from 'react';
|
||||
import { INode } from '~/redux/types';
|
||||
import { apiPostNode } from '~/redux/node/api';
|
||||
|
@ -11,7 +11,7 @@ import { useDispatch } from 'react-redux';
|
|||
|
||||
export const useUpdateNode = (id: number) => {
|
||||
const dispatch = useDispatch();
|
||||
const { update } = useGetNode(id);
|
||||
const { update } = useLoadNode(id);
|
||||
const flowNodes = useShallowSelect(selectFlowNodes);
|
||||
const labNodes = useShallowSelect(selectLabListNodes);
|
||||
|
||||
|
|
|
@ -1,37 +1,32 @@
|
|||
import React, { useEffect, VFC } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useShallowSelect } from '~/hooks/data/useShallowSelect';
|
||||
import { selectNode } from '~/redux/node/selectors';
|
||||
import React, { VFC } from 'react';
|
||||
import { BorisLayout } from '~/layouts/BorisLayout';
|
||||
import { nodeLoadNode } from '~/redux/node/actions';
|
||||
import { CommentContextProvider } from '~/utils/context/CommentContextProvider';
|
||||
import { useImageModal } from '~/hooks/navigation/useImageModal';
|
||||
import { useNodeComments } from '~/hooks/node/useNodeComments';
|
||||
import { useNodeComments } from '~/hooks/comments/useNodeComments';
|
||||
import { useBoris } from '~/hooks/boris/useBoris';
|
||||
import { NodeContextProvider } from '~/utils/context/NodeContextProvider';
|
||||
import { useGetNode } from '~/hooks/node/useGetNode';
|
||||
import { useLoadNode } from '~/hooks/node/useLoadNode';
|
||||
|
||||
const BorisPage: VFC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { node, isLoading, update } = useGetNode(696);
|
||||
const {
|
||||
comments,
|
||||
comment_count: count,
|
||||
is_loading_comments: isLoadingComments,
|
||||
} = useShallowSelect(selectNode);
|
||||
const { node, isLoading, update } = useLoadNode(696);
|
||||
|
||||
const onShowImageModal = useImageModal();
|
||||
const { onLoadMoreComments, onDelete: onDeleteComment } = useNodeComments(696);
|
||||
const {
|
||||
onLoadMoreComments,
|
||||
onDelete: onDeleteComment,
|
||||
onEdit: onSaveComment,
|
||||
comments,
|
||||
hasMore,
|
||||
isLoading: isLoadingComments,
|
||||
} = useNodeComments(696);
|
||||
const { title, setIsBetaTester, isTester, stats } = useBoris(comments);
|
||||
useEffect(() => {
|
||||
dispatch(nodeLoadNode(696, 'DESC'));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<NodeContextProvider node={node} isLoading={isLoading} update={update}>
|
||||
<CommentContextProvider
|
||||
onSaveComment={onSaveComment}
|
||||
comments={comments}
|
||||
count={count}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoadingComments}
|
||||
onShowImageModal={onShowImageModal}
|
||||
onLoadMoreComments={onLoadMoreComments}
|
||||
|
|
|
@ -2,9 +2,8 @@ import React, { FC } from 'react';
|
|||
import { NodeLayout } from '~/layouts/NodeLayout';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { useScrollToTop } from '~/hooks/dom/useScrollToTop';
|
||||
import { useFullNode } from '~/hooks/node/useFullNode';
|
||||
import { useImageModal } from '~/hooks/navigation/useImageModal';
|
||||
import { useNodeComments } from '~/hooks/node/useNodeComments';
|
||||
import { useNodeComments } from '~/hooks/comments/useNodeComments';
|
||||
import { useUser } from '~/hooks/user/userUser';
|
||||
import { useNodeTags } from '~/hooks/node/useNodeTags';
|
||||
import { NodeContextProvider } from '~/utils/context/NodeContextProvider';
|
||||
|
@ -12,7 +11,7 @@ import { CommentContextProvider } from '~/utils/context/CommentContextProvider';
|
|||
import { TagsContextProvider } from '~/utils/context/TagsContextProvider';
|
||||
import { useNodePermissions } from '~/hooks/node/useNodePermissions';
|
||||
import { NodeRelatedProvider } from '~/utils/providers/NodeRelatedProvider';
|
||||
import { useGetNode } from '~/hooks/node/useGetNode';
|
||||
import { useLoadNode } from '~/hooks/node/useLoadNode';
|
||||
|
||||
type Props = RouteComponentProps<{ id: string }> & {};
|
||||
|
||||
|
@ -21,11 +20,17 @@ const NodePage: FC<Props> = ({
|
|||
params: { id },
|
||||
},
|
||||
}) => {
|
||||
const { node, isLoading, update } = useGetNode(parseInt(id, 10));
|
||||
const { isLoadingComments, comments, commentsCount, lastSeenCurrent } = useFullNode(id);
|
||||
const { node, isLoading, update, lastSeen } = useLoadNode(parseInt(id, 10));
|
||||
|
||||
const onShowImageModal = useImageModal();
|
||||
const { onLoadMoreComments, onDelete: onDeleteComment } = useNodeComments(parseInt(id, 10));
|
||||
const {
|
||||
onLoadMoreComments,
|
||||
onDelete: onDeleteComment,
|
||||
onEdit: onSaveComment,
|
||||
comments,
|
||||
hasMore,
|
||||
isLoading: isLoadingComments,
|
||||
} = useNodeComments(parseInt(id, 10));
|
||||
const { onDelete: onTagDelete, onChange: onTagsChange, onClick: onTagClick } = useNodeTags(
|
||||
parseInt(id, 10)
|
||||
);
|
||||
|
@ -43,9 +48,10 @@ const NodePage: FC<Props> = ({
|
|||
<NodeContextProvider node={node} isLoading={isLoading} update={update}>
|
||||
<NodeRelatedProvider id={parseInt(id, 10)} tags={node.tags}>
|
||||
<CommentContextProvider
|
||||
onSaveComment={onSaveComment}
|
||||
comments={comments}
|
||||
count={commentsCount}
|
||||
lastSeenCurrent={lastSeenCurrent}
|
||||
hasMore={hasMore}
|
||||
lastSeenCurrent={lastSeen}
|
||||
isLoading={isLoadingComments}
|
||||
onShowImageModal={onShowImageModal}
|
||||
onLoadMoreComments={onLoadMoreComments}
|
||||
|
|
|
@ -1,67 +1,7 @@
|
|||
import { IComment, IFile, INode, ITag, IValidationErrors } from '../types';
|
||||
import { IFile } from '../types';
|
||||
import { NODE_ACTIONS } from './constants';
|
||||
import { INodeState } from './reducer';
|
||||
|
||||
export const nodeSet = (node: Partial<INodeState>) => ({
|
||||
node,
|
||||
type: NODE_ACTIONS.SET,
|
||||
});
|
||||
|
||||
export const nodeGotoNode = (id: INode['id'], node_type: INode['type']) => ({
|
||||
id,
|
||||
node_type,
|
||||
type: NODE_ACTIONS.GOTO_NODE,
|
||||
});
|
||||
|
||||
export const nodeLoadNode = (id: number, order?: 'ASC' | 'DESC') => ({
|
||||
id,
|
||||
order,
|
||||
type: NODE_ACTIONS.LOAD_NODE,
|
||||
});
|
||||
|
||||
export const nodeSetLoading = (is_loading: INodeState['is_loading']) => ({
|
||||
is_loading,
|
||||
type: NODE_ACTIONS.SET_LOADING,
|
||||
});
|
||||
|
||||
export const nodeSetLoadingComments = (is_loading_comments: INodeState['is_loading_comments']) => ({
|
||||
is_loading_comments,
|
||||
type: NODE_ACTIONS.SET_LOADING_COMMENTS,
|
||||
});
|
||||
|
||||
export const nodePostLocalComment = (
|
||||
nodeId: INode['id'],
|
||||
comment: IComment,
|
||||
callback: (e?: string) => void
|
||||
) => ({
|
||||
nodeId,
|
||||
comment,
|
||||
callback,
|
||||
type: NODE_ACTIONS.POST_LOCAL_COMMENT,
|
||||
});
|
||||
|
||||
export const nodeSetSendingComment = (is_sending_comment: boolean) => ({
|
||||
is_sending_comment,
|
||||
type: NODE_ACTIONS.SET_SENDING_COMMENT,
|
||||
});
|
||||
|
||||
export const nodeSetComments = (comments: IComment[]) => ({
|
||||
comments,
|
||||
type: NODE_ACTIONS.SET_COMMENTS,
|
||||
});
|
||||
|
||||
export const nodeLockComment = (id: number, is_locked: boolean, nodeId: number) => ({
|
||||
type: NODE_ACTIONS.LOCK_COMMENT,
|
||||
nodeId,
|
||||
id,
|
||||
is_locked,
|
||||
});
|
||||
|
||||
export const nodeSetCoverImage = (current_cover_image?: IFile) => ({
|
||||
type: NODE_ACTIONS.SET_COVER_IMAGE,
|
||||
current_cover_image,
|
||||
});
|
||||
|
||||
export const nodeLoadMoreComments = () => ({
|
||||
type: NODE_ACTIONS.LOAD_MORE_COMMENTS,
|
||||
});
|
||||
|
|
|
@ -117,7 +117,7 @@ export const apiLockNode = ({ id, is_locked }: ApiLockNodeRequest) =>
|
|||
.post<ApiLockNodeResult>(API.NODE.POST_LOCK(id), { is_locked })
|
||||
.then(cleanResult);
|
||||
|
||||
export const apiLockComment = ({ id, is_locked, current }: ApiLockCommentRequest) =>
|
||||
export const apiLockComment = ({ id, isLocked, nodeId }: ApiLockCommentRequest) =>
|
||||
api
|
||||
.post<ApiLockcommentResult>(API.NODE.LOCK_COMMENT(current, id), { is_locked })
|
||||
.post<ApiLockcommentResult>(API.NODE.LOCK_COMMENT(nodeId, id), { is_locked: isLocked })
|
||||
.then(cleanResult);
|
||||
|
|
|
@ -25,21 +25,6 @@ import { LabAudio } from '~/components/lab/LabAudioBlock';
|
|||
|
||||
const prefix = 'NODE.';
|
||||
export const NODE_ACTIONS = {
|
||||
LOAD_NODE: `${prefix}LOAD_NODE`,
|
||||
GOTO_NODE: `${prefix}GOTO_NODE`,
|
||||
SET: `${prefix}SET`,
|
||||
|
||||
LOCK_COMMENT: `${prefix}LOCK_COMMENT`,
|
||||
EDIT_COMMENT: `${prefix}EDIT_COMMENT`,
|
||||
LOAD_MORE_COMMENTS: `${prefix}LOAD_MORE_COMMENTS`,
|
||||
|
||||
SET_LOADING: `${prefix}SET_LOADING`,
|
||||
SET_LOADING_COMMENTS: `${prefix}SET_LOADING_COMMENTS`,
|
||||
SET_SENDING_COMMENT: `${prefix}SET_SENDING_COMMENT`,
|
||||
|
||||
POST_LOCAL_COMMENT: `${prefix}POST_LOCAL_COMMENT`,
|
||||
SET_COMMENTS: `${prefix}SET_COMMENTS`,
|
||||
|
||||
SET_COVER_IMAGE: `${prefix}SET_COVER_IMAGE`,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,46 +1,13 @@
|
|||
import { assocPath } from 'ramda';
|
||||
import { NODE_ACTIONS } from './constants';
|
||||
import {
|
||||
nodeSet,
|
||||
nodeSetComments,
|
||||
nodeSetCoverImage,
|
||||
nodeSetLoading,
|
||||
nodeSetLoadingComments,
|
||||
nodeSetSendingComment,
|
||||
} from './actions';
|
||||
import { nodeSetCoverImage } from './actions';
|
||||
import { INodeState } from './reducer';
|
||||
|
||||
const setData = (state: INodeState, { node }: ReturnType<typeof nodeSet>) => ({
|
||||
...state,
|
||||
...node,
|
||||
});
|
||||
|
||||
const setLoading = (state: INodeState, { is_loading }: ReturnType<typeof nodeSetLoading>) =>
|
||||
assocPath(['is_loading'], is_loading, state);
|
||||
|
||||
const setLoadingComments = (
|
||||
state: INodeState,
|
||||
{ is_loading_comments }: ReturnType<typeof nodeSetLoadingComments>
|
||||
) => assocPath(['is_loading_comments'], is_loading_comments, state);
|
||||
|
||||
const setSendingComment = (
|
||||
state: INodeState,
|
||||
{ is_sending_comment }: ReturnType<typeof nodeSetSendingComment>
|
||||
) => assocPath(['is_sending_comment'], is_sending_comment, state);
|
||||
|
||||
const setComments = (state: INodeState, { comments }: ReturnType<typeof nodeSetComments>) =>
|
||||
assocPath(['comments'], comments, state);
|
||||
|
||||
const setCoverImage = (
|
||||
state: INodeState,
|
||||
{ current_cover_image }: ReturnType<typeof nodeSetCoverImage>
|
||||
) => assocPath(['current_cover_image'], current_cover_image, state);
|
||||
|
||||
export const NODE_HANDLERS = {
|
||||
[NODE_ACTIONS.SET]: setData,
|
||||
[NODE_ACTIONS.SET_LOADING]: setLoading,
|
||||
[NODE_ACTIONS.SET_LOADING_COMMENTS]: setLoadingComments,
|
||||
[NODE_ACTIONS.SET_SENDING_COMMENT]: setSendingComment,
|
||||
[NODE_ACTIONS.SET_COMMENTS]: setComments,
|
||||
[NODE_ACTIONS.SET_COVER_IMAGE]: setCoverImage,
|
||||
};
|
||||
|
|
|
@ -1,163 +0,0 @@
|
|||
import { call, put, select, takeLatest, takeLeading } from 'redux-saga/effects';
|
||||
|
||||
import { COMMENTS_DISPLAY, EMPTY_NODE, NODE_ACTIONS } from './constants';
|
||||
import {
|
||||
nodeGotoNode,
|
||||
nodeLoadNode,
|
||||
nodeLockComment,
|
||||
nodePostLocalComment,
|
||||
nodeSet,
|
||||
nodeSetComments,
|
||||
nodeSetLoadingComments,
|
||||
} from './actions';
|
||||
import { apiGetNodeComments, apiLockComment, apiPostComment } from './api';
|
||||
import { flowSetNodes } from '../flow/actions';
|
||||
import { selectFlowNodes } from '../flow/selectors';
|
||||
import { selectNode } from './selectors';
|
||||
import { INode, Unwrap } from '../types';
|
||||
import { showErrorToast } from '~/utils/errors/showToast';
|
||||
|
||||
export function* updateNodeEverywhere(node) {
|
||||
const {
|
||||
current: { id },
|
||||
}: ReturnType<typeof selectNode> = yield select(selectNode);
|
||||
|
||||
const flow_nodes: ReturnType<typeof selectFlowNodes> = yield select(selectFlowNodes);
|
||||
|
||||
yield put(
|
||||
flowSetNodes(
|
||||
flow_nodes
|
||||
.map(flow_node => (flow_node.id === node.id ? node : flow_node))
|
||||
.filter(flow_node => !flow_node.deleted_at)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function* onNodeGoto({ id }: ReturnType<typeof nodeGotoNode>) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield put(nodeLoadNode(id));
|
||||
}
|
||||
|
||||
function* onNodeLoadMoreComments() {
|
||||
try {
|
||||
const {
|
||||
current: { id },
|
||||
comments,
|
||||
}: ReturnType<typeof selectNode> = yield select(selectNode);
|
||||
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data: Unwrap<typeof apiGetNodeComments> = yield call(apiGetNodeComments, {
|
||||
id,
|
||||
take: COMMENTS_DISPLAY,
|
||||
skip: comments.length,
|
||||
});
|
||||
|
||||
const current: ReturnType<typeof selectNode> = yield select(selectNode);
|
||||
|
||||
if (!data || current.current.id != id) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield put(
|
||||
nodeSet({
|
||||
comments: [...comments, ...data.comments],
|
||||
comment_count: data.comment_count,
|
||||
})
|
||||
);
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
function* nodeGetComments(id: INode['id']) {
|
||||
try {
|
||||
const { comments, comment_count }: Unwrap<typeof apiGetNodeComments> = yield call(
|
||||
apiGetNodeComments,
|
||||
{
|
||||
id: id!,
|
||||
take: COMMENTS_DISPLAY,
|
||||
skip: 0,
|
||||
}
|
||||
);
|
||||
|
||||
yield put(
|
||||
nodeSet({
|
||||
comments,
|
||||
comment_count,
|
||||
})
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function* onNodeLoad({ id }: ReturnType<typeof nodeLoadNode>) {
|
||||
// Comments
|
||||
try {
|
||||
yield put(nodeSetLoadingComments(true));
|
||||
yield call(nodeGetComments, id);
|
||||
|
||||
yield put(
|
||||
nodeSet({
|
||||
is_loading_comments: false,
|
||||
})
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function* onPostComment({ nodeId, comment, callback }: ReturnType<typeof nodePostLocalComment>) {
|
||||
try {
|
||||
const data: Unwrap<typeof apiPostComment> = yield call(apiPostComment, {
|
||||
data: comment,
|
||||
id: nodeId,
|
||||
});
|
||||
|
||||
const { comments }: ReturnType<typeof selectNode> = yield select(selectNode);
|
||||
|
||||
if (!comment.id) {
|
||||
yield put(nodeSetComments([data.comment, ...comments]));
|
||||
} else {
|
||||
yield put(
|
||||
nodeSet({
|
||||
comments: comments.map(item => (item.id === comment.id ? data.comment : item)),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
callback();
|
||||
} catch (error) {
|
||||
return callback(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function* onLockCommentSaga({ nodeId, id, is_locked }: ReturnType<typeof nodeLockComment>) {
|
||||
const { comments }: ReturnType<typeof selectNode> = yield select(selectNode);
|
||||
|
||||
try {
|
||||
const data: Unwrap<typeof apiLockComment> = yield call(apiLockComment, {
|
||||
current: nodeId,
|
||||
id,
|
||||
is_locked,
|
||||
});
|
||||
|
||||
yield put(
|
||||
nodeSetComments(
|
||||
comments.map(comment =>
|
||||
comment.id === id ? { ...comment, deleted_at: data.deleted_at || undefined } : comment
|
||||
)
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
showErrorToast(e);
|
||||
}
|
||||
}
|
||||
|
||||
export default function* nodeSaga() {
|
||||
yield takeLatest(NODE_ACTIONS.GOTO_NODE, onNodeGoto);
|
||||
yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad);
|
||||
yield takeLatest(NODE_ACTIONS.POST_LOCAL_COMMENT, onPostComment);
|
||||
yield takeLatest(NODE_ACTIONS.LOCK_COMMENT, onLockCommentSaga);
|
||||
yield takeLeading(NODE_ACTIONS.LOAD_MORE_COMMENTS, onNodeLoadMoreComments);
|
||||
}
|
|
@ -79,8 +79,8 @@ export type ApiLockNodeResult = {
|
|||
|
||||
export type ApiLockCommentRequest = {
|
||||
id: IComment['id'];
|
||||
current: INode['id'];
|
||||
is_locked: boolean;
|
||||
nodeId: INode['id'];
|
||||
isLocked: boolean;
|
||||
};
|
||||
export type ApiLockcommentResult = {
|
||||
deleted_at: string;
|
||||
|
|
|
@ -12,7 +12,6 @@ import authSaga from '~/redux/auth/sagas';
|
|||
import { IAuthState } from '~/redux/auth/types';
|
||||
|
||||
import node, { INodeState } from '~/redux/node/reducer';
|
||||
import nodeSaga from '~/redux/node/sagas';
|
||||
|
||||
import flow, { IFlowState } from '~/redux/flow/reducer';
|
||||
import flowSaga from '~/redux/flow/sagas';
|
||||
|
@ -108,7 +107,6 @@ export function configureStore(): {
|
|||
persistor: Persistor;
|
||||
} {
|
||||
sagaMiddleware.run(authSaga);
|
||||
sagaMiddleware.run(nodeSaga);
|
||||
sagaMiddleware.run(uploadSaga);
|
||||
sagaMiddleware.run(flowSaga);
|
||||
sagaMiddleware.run(playerSaga);
|
||||
|
|
|
@ -2,22 +2,23 @@ import { IComment, IFile } from '~/redux/types';
|
|||
import React, { createContext, FC, useContext } from 'react';
|
||||
|
||||
export interface CommentProviderProps {
|
||||
// user: IUser;
|
||||
comments: IComment[];
|
||||
count: number;
|
||||
hasMore: boolean;
|
||||
lastSeenCurrent?: string;
|
||||
isLoading: boolean;
|
||||
onShowImageModal: (images: IFile[], index: number) => void;
|
||||
onLoadMoreComments: () => void;
|
||||
onSaveComment: (comment: IComment) => Promise<unknown>;
|
||||
onDeleteComment: (id: IComment['id'], isLocked: boolean) => void;
|
||||
}
|
||||
|
||||
const CommentContext = createContext<CommentProviderProps>({
|
||||
// user: EMPTY_USER,
|
||||
comments: [],
|
||||
count: 0,
|
||||
hasMore: false,
|
||||
lastSeenCurrent: undefined,
|
||||
isLoading: false,
|
||||
onSaveComment: async () => {},
|
||||
onShowImageModal: () => {},
|
||||
onLoadMoreComments: () => {},
|
||||
onDeleteComment: () => {},
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
const handle = (message: string) => console.warn(message);
|
||||
|
||||
export const showErrorToast = (error: unknown) => {
|
||||
if (typeof error === 'string') {
|
||||
handle(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(error instanceof Error)) {
|
||||
console.warn('catched strange exception', error);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: show toast or something
|
||||
console.warn(error.message);
|
||||
handle(error.message);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue