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

removed almost all node sagas

This commit is contained in:
Fedor Katurov 2022-01-02 20:44:41 +07:00
parent f76a5a4798
commit 168ba8cc04
30 changed files with 268 additions and 448 deletions

View file

@ -14,6 +14,7 @@ type IProps = HTMLAttributes<HTMLDivElement> & {
group: ICommentGroup; group: ICommentGroup;
isSame?: boolean; isSame?: boolean;
canEdit?: boolean; canEdit?: boolean;
saveComment: (data: IComment) => Promise<unknown>;
onDelete: (id: IComment['id'], isLocked: boolean) => void; onDelete: (id: IComment['id'], isLocked: boolean) => void;
onShowImageModal: (images: IFile[], index: number) => void; onShowImageModal: (images: IFile[], index: number) => void;
}; };
@ -29,6 +30,7 @@ const Comment: FC<IProps> = memo(
canEdit, canEdit,
onDelete, onDelete,
onShowImageModal, onShowImageModal,
saveComment,
...props ...props
}) => { }) => {
return ( return (
@ -50,6 +52,7 @@ const Comment: FC<IProps> = memo(
return ( return (
<CommentContent <CommentContent
saveComment={saveComment}
nodeId={nodeId} nodeId={nodeId}
comment={comment} comment={comment}
key={comment.id} key={comment.id}

View file

@ -18,12 +18,13 @@ interface IProps {
nodeId: number; nodeId: number;
comment: IComment; comment: IComment;
canEdit: boolean; canEdit: boolean;
saveComment: (data: IComment) => Promise<unknown>;
onDelete: (id: IComment['id'], isLocked: boolean) => void; onDelete: (id: IComment['id'], isLocked: boolean) => void;
onShowImageModal: (images: IFile[], index: number) => void; onShowImageModal: (images: IFile[], index: number) => void;
} }
const CommentContent: FC<IProps> = memo( const CommentContent: FC<IProps> = memo(
({ comment, canEdit, nodeId, onDelete, onShowImageModal }) => { ({ comment, canEdit, nodeId, saveComment, onDelete, onShowImageModal }) => {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]); const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]);
@ -58,7 +59,14 @@ const CommentContent: FC<IProps> = memo(
); );
if (isEditing) { if (isEditing) {
return <CommentForm nodeId={nodeId} comment={comment} onCancelEdit={stopEditing} />; return (
<CommentForm
saveComment={saveComment}
nodeId={nodeId}
comment={comment}
onCancelEdit={stopEditing}
/>
);
} }
return ( return (

View file

@ -20,17 +20,24 @@ import { Filler } from '~/components/containers/Filler';
interface IProps { interface IProps {
comment?: IComment; comment?: IComment;
nodeId: INode['id']; nodeId: INode['id'];
saveComment: (data: IComment) => Promise<unknown>;
onCancelEdit?: () => void; onCancelEdit?: () => void;
} }
const CommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => { const CommentForm: FC<IProps> = ({ comment, nodeId, saveComment, onCancelEdit }) => {
const [textarea, setTextarea] = useState<HTMLTextAreaElement>(); const [textarea, setTextarea] = useState<HTMLTextAreaElement>();
const uploader = useFileUploader( const uploader = useFileUploader(
UPLOAD_SUBJECTS.COMMENT, UPLOAD_SUBJECTS.COMMENT,
UPLOAD_TARGETS.COMMENTS, UPLOAD_TARGETS.COMMENTS,
comment?.files 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 isLoading = formik.isSubmitting || uploader.isUploading;
const isEditing = !!comment?.id; const isEditing = !!comment?.id;

View file

@ -1,27 +1,21 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { CommentWrapper } from '~/components/containers/CommentWrapper'; import { CommentWrapper } from '~/components/containers/CommentWrapper';
import { connect } from 'react-redux';
import { selectAuthUser } from '~/redux/auth/selectors';
import { CommentForm } from '~/components/comment/CommentForm'; 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 => ({ interface NodeCommentFormProps {
user: selectAuthUser(state), user: IUser;
}); nodeId?: number;
saveComment: (comment: IComment) => Promise<unknown>;
}
type IProps = ReturnType<typeof mapStateToProps> & { const NodeCommentForm: FC<NodeCommentFormProps> = ({ user, nodeId, saveComment }) => {
isBefore?: boolean;
nodeId: INode['id'];
};
const NodeCommentFormUnconnected: FC<IProps> = ({ user, isBefore, nodeId }) => {
return ( return (
<CommentWrapper user={user} isForm> <CommentWrapper user={user} isForm>
<CommentForm nodeId={nodeId} /> <CommentForm nodeId={nodeId} saveComment={saveComment} />
</CommentWrapper> </CommentWrapper>
); );
}; };
const NodeCommentForm = connect(mapStateToProps)(NodeCommentFormUnconnected);
export { NodeCommentForm }; export { NodeCommentForm };

View file

@ -27,7 +27,7 @@ export const API = {
GET_DIFF: '/flow/diff', GET_DIFF: '/flow/diff',
GET_NODE: (id: number | string) => `/node/${id}`, 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`, RELATED: (id: INode['id']) => `/node/${id}/related`,
UPDATE_TAGS: (id: INode['id']) => `/node/${id}/tags`, UPDATE_TAGS: (id: INode['id']) => `/node/${id}/tags`,
DELETE_TAG: (id: INode['id'], tagId: ITag['ID']) => `/node/${id}/tags/${tagId}`, DELETE_TAG: (id: INode['id'], tagId: ITag['ID']) => `/node/${id}/tags/${tagId}`,

View file

@ -16,24 +16,28 @@ const BorisComments: FC<IProps> = () => {
const { const {
isLoading, isLoading,
comments, comments,
onSaveComment,
onLoadMoreComments, onLoadMoreComments,
onDeleteComment, onDeleteComment,
onShowImageModal, onShowImageModal,
count, hasMore,
} = useCommentContext(); } = useCommentContext();
const { node } = useNodeContext(); const { node } = useNodeContext();
return ( return (
<> <>
<Group className={styles.grid}> <Group className={styles.grid}>
{user.is_user && <NodeCommentForm isBefore nodeId={node.id} />} {user.is_user && (
<NodeCommentForm user={user} nodeId={node.id} saveComment={onSaveComment} />
)}
{isLoading ? ( {isLoading ? (
<NodeNoComments is_loading count={7} /> <NodeNoComments is_loading count={7} />
) : ( ) : (
<CommentContextProvider <CommentContextProvider
onSaveComment={onSaveComment}
comments={comments} comments={comments}
count={count} hasMore={hasMore}
onDeleteComment={onDeleteComment} onDeleteComment={onDeleteComment}
onLoadMoreComments={onLoadMoreComments} onLoadMoreComments={onLoadMoreComments}
onShowImageModal={onShowImageModal} onShowImageModal={onShowImageModal}

View file

@ -4,7 +4,7 @@ import { useHistory, useRouteMatch } from 'react-router';
import { ModalWrapper } from '~/components/dialogs/ModalWrapper'; import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
import { LoaderCircle } from '~/components/input/LoaderCircle'; import { LoaderCircle } from '~/components/input/LoaderCircle';
import styles from './styles.module.scss'; 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 { useUpdateNode } from '~/hooks/node/useUpdateNode';
import { INode } from '~/redux/types'; import { INode } from '~/redux/types';
@ -24,7 +24,7 @@ const EditorEditDialog: FC = () => {
history.replace(backUrl); history.replace(backUrl);
}, [backUrl, history]); }, [backUrl, history]);
const { node, isLoading } = useGetNode(parseInt(id, 10)); const { node, isLoading } = useLoadNode(parseInt(id, 10));
const updateNode = useUpdateNode(parseInt(id, 10)); const updateNode = useUpdateNode(parseInt(id, 10));
const onSubmit = useCallback( const onSubmit = useCallback(

View file

@ -21,9 +21,9 @@ interface IProps {
} }
const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => { const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => {
const { is_user: isUser } = useUserContext(); const user = useUserContext();
const { node, isLoading } = useNodeContext(); const { node, isLoading } = useNodeContext();
const { comments, isLoading: isLoadingComments } = useCommentContext(); const { comments, isLoading: isLoadingComments, onSaveComment } = useCommentContext();
const { related, isLoading: isLoadingRelated } = useNodeRelatedContext(); const { related, isLoading: isLoadingRelated } = useNodeRelatedContext();
const { inline } = useNodeBlocks(node, isLoading); const { inline } = useNodeBlocks(node, isLoading);
@ -44,7 +44,9 @@ const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => {
<NodeComments order={commentsOrder} /> <NodeComments order={commentsOrder} />
)} )}
{isUser && !isLoading && <NodeCommentForm nodeId={node.id} />} {user.is_user && !isLoading && (
<NodeCommentForm nodeId={node.id} saveComment={onSaveComment} user={user} />
)}
</Group> </Group>
<div className={styles.panel}> <div className={styles.panel}>

View file

@ -21,27 +21,24 @@ const NodeComments: FC<IProps> = memo(({ order }) => {
const { const {
comments, comments,
count, hasMore,
lastSeenCurrent, lastSeenCurrent,
onLoadMoreComments, onLoadMoreComments,
onDeleteComment, onDeleteComment,
onShowImageModal, onShowImageModal,
onSaveComment,
} = useCommentContext(); } = useCommentContext();
const left = useMemo(() => Math.max(0, count - comments.length), [comments, count]);
const groupped: ICommentGroup[] = useGrouppedComments(comments, order, lastSeenCurrent); const groupped: ICommentGroup[] = useGrouppedComments(comments, order, lastSeenCurrent);
const more = useMemo( const more = useMemo(
() => () =>
left > 0 && ( hasMore && (
<div className={styles.more} onClick={onLoadMoreComments}> <div className={styles.more} onClick={onLoadMoreComments}>
Показать ещё{' '} Показать ещё комментарии
{plural(Math.min(left, COMMENTS_DISPLAY), 'комментарий', 'комментария', 'комментариев')}
{left > COMMENTS_DISPLAY ? ` из ${left} оставшихся` : ''}
</div> </div>
), ),
[left, onLoadMoreComments] [hasMore, onLoadMoreComments]
); );
if (!node?.id) { if (!node?.id) {
@ -61,6 +58,7 @@ const NodeComments: FC<IProps> = memo(({ order }) => {
onDelete={onDeleteComment} onDelete={onDeleteComment}
onShowImageModal={onShowImageModal} onShowImageModal={onShowImageModal}
isSame={group.user.id === user.id} isSame={group.user.id === user.id}
saveComment={onSaveComment}
/> />
))} ))}

View file

@ -41,7 +41,7 @@ const ProfileLayoutUnconnected: FC<IProps> = ({ history, nodeSetCoverImage }) =>
<Grid className={styles.content}> <Grid className={styles.content}>
<div className={styles.comments}> <div className={styles.comments}>
<CommentForm nodeId={0} /> <CommentForm nodeId={0} saveComment={async () => console.log()} />
<NodeNoComments is_loading={false} /> <NodeNoComments is_loading={false} />
</div> </div>
</Grid> </Grid>

View file

@ -3,21 +3,23 @@ import { useCallback, useEffect, useRef } from 'react';
import { FormikHelpers, useFormik, useFormikContext } from 'formik'; import { FormikHelpers, useFormik, useFormikContext } from 'formik';
import { array, object, string } from 'yup'; import { array, object, string } from 'yup';
import { FileUploader } from '~/hooks/data/useFileUploader'; import { FileUploader } from '~/hooks/data/useFileUploader';
import { useDispatch } from 'react-redux'; import { showErrorToast } from '~/utils/errors/showToast';
import { nodePostLocalComment } from '~/redux/node/actions'; import { hasPath, path } from 'ramda';
const validationSchema = object().shape({ const validationSchema = object().shape({
text: string(), text: string(),
files: array(), files: array(),
}); });
const onSuccess = ({ resetForm, setStatus, setSubmitting }: FormikHelpers<IComment>) => ( const onSuccess = ({ resetForm, setSubmitting, setErrors }: FormikHelpers<IComment>) => (
e?: string error?: unknown
) => { ) => {
setSubmitting(false); setSubmitting(false);
if (e) { if (hasPath(['response', 'data', 'error'], error)) {
setStatus(e); const message = path(['response', 'data', 'error'], error) as string;
setErrors({ text: message });
showErrorToast(error);
return; return;
} }
@ -30,26 +32,23 @@ export const useCommentFormFormik = (
values: IComment, values: IComment,
nodeId: INode['id'], nodeId: INode['id'],
uploader: FileUploader, uploader: FileUploader,
sendData: (data: IComment) => Promise<unknown>,
stopEditing?: () => void stopEditing?: () => void
) => { ) => {
const dispatch = useDispatch();
const { current: initialValues } = useRef(values); const { current: initialValues } = useRef(values);
const onSubmit = useCallback( const onSubmit = useCallback(
(values: IComment, helpers: FormikHelpers<IComment>) => { async (values: IComment, helpers: FormikHelpers<IComment>) => {
try {
helpers.setSubmitting(true); helpers.setSubmitting(true);
dispatch( await sendData(values);
nodePostLocalComment( onSuccess(helpers)();
nodeId, } catch (error) {
{ console.log('error', error);
...values, onSuccess(helpers)(error);
files: uploader.files, }
}, },
onSuccess(helpers) [sendData]
)
);
},
[dispatch, nodeId, uploader.files]
); );
const onReset = useCallback(() => { const onReset = useCallback(() => {

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

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

View file

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

View file

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

View file

@ -1,12 +1,35 @@
import { useEffect } from 'react'; import useSWR from 'swr';
import { nodeGotoNode } from '~/redux/node/actions'; import { ApiGetNodeResponse } from '~/redux/node/types';
import { useDispatch } from 'react-redux'; 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: number) => {
export const useLoadNode = (id: any) => { const { data, isValidating, mutate } = useSWR<ApiGetNodeResponse>(API.NODE.GET_NODE(id), () =>
const dispatch = useDispatch(); apiGetNode({ id })
);
useEffect(() => { const update = useCallback(
dispatch(nodeGotoNode(parseInt(id, 10), undefined)); async (node?: Partial<INode>) => {
}, [dispatch, id]); 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,
};
}; };

View file

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

View file

@ -2,11 +2,11 @@ import { useHistory } from 'react-router';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { ITag } from '~/redux/types'; import { ITag } from '~/redux/types';
import { URLS } from '~/constants/urls'; import { URLS } from '~/constants/urls';
import { useGetNode } from '~/hooks/node/useGetNode'; import { useLoadNode } from '~/hooks/node/useLoadNode';
import { apiDeleteNodeTag, apiPostNodeTags } from '~/redux/node/api'; import { apiDeleteNodeTag, apiPostNodeTags } from '~/redux/node/api';
export const useNodeTags = (id: number) => { export const useNodeTags = (id: number) => {
const { update } = useGetNode(id); const { update } = useLoadNode(id);
const history = useHistory(); const history = useHistory();
const onChange = useCallback( const onChange = useCallback(

View file

@ -1,4 +1,4 @@
import { useGetNode } from '~/hooks/node/useGetNode'; import { useLoadNode } from '~/hooks/node/useLoadNode';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { INode } from '~/redux/types'; import { INode } from '~/redux/types';
import { apiPostNode } from '~/redux/node/api'; import { apiPostNode } from '~/redux/node/api';
@ -11,7 +11,7 @@ import { useDispatch } from 'react-redux';
export const useUpdateNode = (id: number) => { export const useUpdateNode = (id: number) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { update } = useGetNode(id); const { update } = useLoadNode(id);
const flowNodes = useShallowSelect(selectFlowNodes); const flowNodes = useShallowSelect(selectFlowNodes);
const labNodes = useShallowSelect(selectLabListNodes); const labNodes = useShallowSelect(selectLabListNodes);

View file

@ -1,37 +1,32 @@
import React, { useEffect, VFC } from 'react'; import React, { VFC } from 'react';
import { useDispatch } from 'react-redux';
import { useShallowSelect } from '~/hooks/data/useShallowSelect';
import { selectNode } from '~/redux/node/selectors';
import { BorisLayout } from '~/layouts/BorisLayout'; import { BorisLayout } from '~/layouts/BorisLayout';
import { nodeLoadNode } from '~/redux/node/actions';
import { CommentContextProvider } from '~/utils/context/CommentContextProvider'; import { CommentContextProvider } from '~/utils/context/CommentContextProvider';
import { useImageModal } from '~/hooks/navigation/useImageModal'; 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 { useBoris } from '~/hooks/boris/useBoris';
import { NodeContextProvider } from '~/utils/context/NodeContextProvider'; import { NodeContextProvider } from '~/utils/context/NodeContextProvider';
import { useGetNode } from '~/hooks/node/useGetNode'; import { useLoadNode } from '~/hooks/node/useLoadNode';
const BorisPage: VFC = () => { const BorisPage: VFC = () => {
const dispatch = useDispatch(); const { node, isLoading, update } = useLoadNode(696);
const { node, isLoading, update } = useGetNode(696);
const {
comments,
comment_count: count,
is_loading_comments: isLoadingComments,
} = useShallowSelect(selectNode);
const onShowImageModal = useImageModal(); 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); const { title, setIsBetaTester, isTester, stats } = useBoris(comments);
useEffect(() => {
dispatch(nodeLoadNode(696, 'DESC'));
}, [dispatch]);
return ( return (
<NodeContextProvider node={node} isLoading={isLoading} update={update}> <NodeContextProvider node={node} isLoading={isLoading} update={update}>
<CommentContextProvider <CommentContextProvider
onSaveComment={onSaveComment}
comments={comments} comments={comments}
count={count} hasMore={hasMore}
isLoading={isLoadingComments} isLoading={isLoadingComments}
onShowImageModal={onShowImageModal} onShowImageModal={onShowImageModal}
onLoadMoreComments={onLoadMoreComments} onLoadMoreComments={onLoadMoreComments}

View file

@ -2,9 +2,8 @@ import React, { FC } from 'react';
import { NodeLayout } from '~/layouts/NodeLayout'; import { NodeLayout } from '~/layouts/NodeLayout';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import { useScrollToTop } from '~/hooks/dom/useScrollToTop'; import { useScrollToTop } from '~/hooks/dom/useScrollToTop';
import { useFullNode } from '~/hooks/node/useFullNode';
import { useImageModal } from '~/hooks/navigation/useImageModal'; 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 { useUser } from '~/hooks/user/userUser';
import { useNodeTags } from '~/hooks/node/useNodeTags'; import { useNodeTags } from '~/hooks/node/useNodeTags';
import { NodeContextProvider } from '~/utils/context/NodeContextProvider'; import { NodeContextProvider } from '~/utils/context/NodeContextProvider';
@ -12,7 +11,7 @@ import { CommentContextProvider } from '~/utils/context/CommentContextProvider';
import { TagsContextProvider } from '~/utils/context/TagsContextProvider'; import { TagsContextProvider } from '~/utils/context/TagsContextProvider';
import { useNodePermissions } from '~/hooks/node/useNodePermissions'; import { useNodePermissions } from '~/hooks/node/useNodePermissions';
import { NodeRelatedProvider } from '~/utils/providers/NodeRelatedProvider'; import { NodeRelatedProvider } from '~/utils/providers/NodeRelatedProvider';
import { useGetNode } from '~/hooks/node/useGetNode'; import { useLoadNode } from '~/hooks/node/useLoadNode';
type Props = RouteComponentProps<{ id: string }> & {}; type Props = RouteComponentProps<{ id: string }> & {};
@ -21,11 +20,17 @@ const NodePage: FC<Props> = ({
params: { id }, params: { id },
}, },
}) => { }) => {
const { node, isLoading, update } = useGetNode(parseInt(id, 10)); const { node, isLoading, update, lastSeen } = useLoadNode(parseInt(id, 10));
const { isLoadingComments, comments, commentsCount, lastSeenCurrent } = useFullNode(id);
const onShowImageModal = useImageModal(); 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( const { onDelete: onTagDelete, onChange: onTagsChange, onClick: onTagClick } = useNodeTags(
parseInt(id, 10) parseInt(id, 10)
); );
@ -43,9 +48,10 @@ const NodePage: FC<Props> = ({
<NodeContextProvider node={node} isLoading={isLoading} update={update}> <NodeContextProvider node={node} isLoading={isLoading} update={update}>
<NodeRelatedProvider id={parseInt(id, 10)} tags={node.tags}> <NodeRelatedProvider id={parseInt(id, 10)} tags={node.tags}>
<CommentContextProvider <CommentContextProvider
onSaveComment={onSaveComment}
comments={comments} comments={comments}
count={commentsCount} hasMore={hasMore}
lastSeenCurrent={lastSeenCurrent} lastSeenCurrent={lastSeen}
isLoading={isLoadingComments} isLoading={isLoadingComments}
onShowImageModal={onShowImageModal} onShowImageModal={onShowImageModal}
onLoadMoreComments={onLoadMoreComments} onLoadMoreComments={onLoadMoreComments}

View file

@ -1,67 +1,7 @@
import { IComment, IFile, INode, ITag, IValidationErrors } from '../types'; import { IFile } from '../types';
import { NODE_ACTIONS } from './constants'; 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) => ({ export const nodeSetCoverImage = (current_cover_image?: IFile) => ({
type: NODE_ACTIONS.SET_COVER_IMAGE, type: NODE_ACTIONS.SET_COVER_IMAGE,
current_cover_image, current_cover_image,
}); });
export const nodeLoadMoreComments = () => ({
type: NODE_ACTIONS.LOAD_MORE_COMMENTS,
});

View file

@ -117,7 +117,7 @@ export const apiLockNode = ({ id, is_locked }: ApiLockNodeRequest) =>
.post<ApiLockNodeResult>(API.NODE.POST_LOCK(id), { is_locked }) .post<ApiLockNodeResult>(API.NODE.POST_LOCK(id), { is_locked })
.then(cleanResult); .then(cleanResult);
export const apiLockComment = ({ id, is_locked, current }: ApiLockCommentRequest) => export const apiLockComment = ({ id, isLocked, nodeId }: ApiLockCommentRequest) =>
api api
.post<ApiLockcommentResult>(API.NODE.LOCK_COMMENT(current, id), { is_locked }) .post<ApiLockcommentResult>(API.NODE.LOCK_COMMENT(nodeId, id), { is_locked: isLocked })
.then(cleanResult); .then(cleanResult);

View file

@ -25,21 +25,6 @@ import { LabAudio } from '~/components/lab/LabAudioBlock';
const prefix = 'NODE.'; const prefix = 'NODE.';
export const NODE_ACTIONS = { 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`, SET_COVER_IMAGE: `${prefix}SET_COVER_IMAGE`,
}; };

View file

@ -1,46 +1,13 @@
import { assocPath } from 'ramda'; import { assocPath } from 'ramda';
import { NODE_ACTIONS } from './constants'; import { NODE_ACTIONS } from './constants';
import { import { nodeSetCoverImage } from './actions';
nodeSet,
nodeSetComments,
nodeSetCoverImage,
nodeSetLoading,
nodeSetLoadingComments,
nodeSetSendingComment,
} from './actions';
import { INodeState } from './reducer'; 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 = ( const setCoverImage = (
state: INodeState, state: INodeState,
{ current_cover_image }: ReturnType<typeof nodeSetCoverImage> { current_cover_image }: ReturnType<typeof nodeSetCoverImage>
) => assocPath(['current_cover_image'], current_cover_image, state); ) => assocPath(['current_cover_image'], current_cover_image, state);
export const NODE_HANDLERS = { 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, [NODE_ACTIONS.SET_COVER_IMAGE]: setCoverImage,
}; };

View file

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

View file

@ -79,8 +79,8 @@ export type ApiLockNodeResult = {
export type ApiLockCommentRequest = { export type ApiLockCommentRequest = {
id: IComment['id']; id: IComment['id'];
current: INode['id']; nodeId: INode['id'];
is_locked: boolean; isLocked: boolean;
}; };
export type ApiLockcommentResult = { export type ApiLockcommentResult = {
deleted_at: string; deleted_at: string;

View file

@ -12,7 +12,6 @@ import authSaga from '~/redux/auth/sagas';
import { IAuthState } from '~/redux/auth/types'; import { IAuthState } from '~/redux/auth/types';
import node, { INodeState } from '~/redux/node/reducer'; import node, { INodeState } from '~/redux/node/reducer';
import nodeSaga from '~/redux/node/sagas';
import flow, { IFlowState } from '~/redux/flow/reducer'; import flow, { IFlowState } from '~/redux/flow/reducer';
import flowSaga from '~/redux/flow/sagas'; import flowSaga from '~/redux/flow/sagas';
@ -108,7 +107,6 @@ export function configureStore(): {
persistor: Persistor; persistor: Persistor;
} { } {
sagaMiddleware.run(authSaga); sagaMiddleware.run(authSaga);
sagaMiddleware.run(nodeSaga);
sagaMiddleware.run(uploadSaga); sagaMiddleware.run(uploadSaga);
sagaMiddleware.run(flowSaga); sagaMiddleware.run(flowSaga);
sagaMiddleware.run(playerSaga); sagaMiddleware.run(playerSaga);

View file

@ -2,22 +2,23 @@ import { IComment, IFile } from '~/redux/types';
import React, { createContext, FC, useContext } from 'react'; import React, { createContext, FC, useContext } from 'react';
export interface CommentProviderProps { export interface CommentProviderProps {
// user: IUser;
comments: IComment[]; comments: IComment[];
count: number; hasMore: boolean;
lastSeenCurrent?: string; lastSeenCurrent?: string;
isLoading: boolean; isLoading: boolean;
onShowImageModal: (images: IFile[], index: number) => void; onShowImageModal: (images: IFile[], index: number) => void;
onLoadMoreComments: () => void; onLoadMoreComments: () => void;
onSaveComment: (comment: IComment) => Promise<unknown>;
onDeleteComment: (id: IComment['id'], isLocked: boolean) => void; onDeleteComment: (id: IComment['id'], isLocked: boolean) => void;
} }
const CommentContext = createContext<CommentProviderProps>({ const CommentContext = createContext<CommentProviderProps>({
// user: EMPTY_USER, // user: EMPTY_USER,
comments: [], comments: [],
count: 0, hasMore: false,
lastSeenCurrent: undefined, lastSeenCurrent: undefined,
isLoading: false, isLoading: false,
onSaveComment: async () => {},
onShowImageModal: () => {}, onShowImageModal: () => {},
onLoadMoreComments: () => {}, onLoadMoreComments: () => {},
onDeleteComment: () => {}, onDeleteComment: () => {},

View file

@ -1,9 +1,15 @@
const handle = (message: string) => console.warn(message);
export const showErrorToast = (error: unknown) => { export const showErrorToast = (error: unknown) => {
if (typeof error === 'string') {
handle(error);
return;
}
if (!(error instanceof Error)) { if (!(error instanceof Error)) {
console.warn('catched strange exception', error); console.warn('catched strange exception', error);
return; return;
} }
// TODO: show toast or something handle(error.message);
console.warn(error.message);
}; };