From c2d1c2bfc9b76e7c5e9e3d241c90d82ab3363295 Mon Sep 17 00:00:00 2001 From: muerwre <33246675+muerwre@users.noreply.github.com> Date: Sun, 2 Jan 2022 17:10:21 +0700 Subject: [PATCH] 99 use swr (#100) * 99: made node use SWR * 99: fixed comments for SWR node * 99: added error toast to useNodeFormFormik.ts --- src/components/comment/Comment/index.tsx | 17 +- .../comment/CommentContent/index.tsx | 162 ++++++++-------- src/components/editors/AudioEditor/index.tsx | 2 +- .../editors/EditorActionsPanel/index.tsx | 3 +- .../editors/EditorButtons/index.tsx | 3 +- .../editors/EditorPublicSwitch/index.tsx | 2 +- .../editors/EditorUploadButton/index.tsx | 2 +- .../editors/EditorUploadCoverButton/index.tsx | 2 +- src/components/editors/TextEditor/index.tsx | 2 +- src/components/editors/VideoEditor/index.tsx | 2 +- .../node/NodeImageSwiperBlock/index.tsx | 1 - .../dialogs/EditorCreateDialog/index.tsx | 16 +- src/containers/dialogs/EditorDialog/index.tsx | 9 +- .../dialogs/EditorEditDialog/index.tsx | 14 +- src/containers/node/NodeComments/index.tsx | 8 + src/pages/node/[id].tsx | 16 +- src/redux/node/actions.ts | 47 +---- src/redux/node/api.ts | 8 +- src/redux/node/constants.ts | 9 +- src/redux/node/handlers.ts | 5 - src/redux/node/sagas.ts | 178 ++---------------- src/utils/errors/showToast.ts | 9 + src/utils/hooks/data/useCreateNode.ts | 34 ++++ src/utils/hooks/data/useGetNode.ts | 32 ++-- src/utils/hooks/data/useUpdateNode.ts | 43 +++++ src/utils/hooks/node/useFullNode.ts | 2 +- src/utils/hooks/node/useLoadNode.ts | 7 +- src/utils/hooks/node/useNodeActions.ts | 13 +- src/utils/hooks/node/useNodeComments.ts | 8 +- .../hooks/{ => node}/useNodeFormFormik.ts | 47 ++--- src/utils/hooks/node/useNodeImages.ts | 4 - src/utils/hooks/node/useNodePermissions.ts | 2 +- src/utils/hooks/node/useNodeTags.ts | 32 ++-- src/utils/hooks/node/useOnNodeSeen.ts | 21 ++- src/utils/node.ts | 17 +- 35 files changed, 366 insertions(+), 413 deletions(-) create mode 100644 src/utils/errors/showToast.ts create mode 100644 src/utils/hooks/data/useCreateNode.ts create mode 100644 src/utils/hooks/data/useUpdateNode.ts rename src/utils/hooks/{ => node}/useNodeFormFormik.ts (52%) diff --git a/src/components/comment/Comment/index.tsx b/src/components/comment/Comment/index.tsx index 50d9cd82..4f2ee99b 100644 --- a/src/components/comment/Comment/index.tsx +++ b/src/components/comment/Comment/index.tsx @@ -8,8 +8,9 @@ import classNames from 'classnames'; import { NEW_COMMENT_CLASSNAME } from '~/constants/comment'; type IProps = HTMLAttributes & { - is_empty?: boolean; - is_loading?: boolean; + nodeId: number; + isEmpty?: boolean; + isLoading?: boolean; group: ICommentGroup; isSame?: boolean; canEdit?: boolean; @@ -20,9 +21,10 @@ type IProps = HTMLAttributes & { const Comment: FC = memo( ({ group, - is_empty, + nodeId, + isEmpty, isSame, - is_loading, + isLoading, className, canEdit, onDelete, @@ -34,8 +36,8 @@ const Comment: FC = memo( className={classNames(className, { [NEW_COMMENT_CLASSNAME]: group.hasNew, })} - isEmpty={is_empty} - isLoading={is_loading} + isEmpty={isEmpty} + isLoading={isLoading} user={group.user} isNew={group.hasNew && !isSame} {...props} @@ -48,9 +50,10 @@ const Comment: FC = memo( return ( diff --git a/src/components/comment/CommentContent/index.tsx b/src/components/comment/CommentContent/index.tsx index bc280fbc..e8b79667 100644 --- a/src/components/comment/CommentContent/index.tsx +++ b/src/components/comment/CommentContent/index.tsx @@ -13,106 +13,108 @@ import { PRESETS } from '~/constants/urls'; import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment'; import { CommentMenu } from '../CommentMenu'; import { CommentForm } from '~/components/comment/CommentForm'; -import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; -import { selectNode } from '~/redux/node/selectors'; interface IProps { + nodeId: number; comment: IComment; - can_edit: boolean; + canEdit: boolean; onDelete: (id: IComment['id'], isLocked: boolean) => void; onShowImageModal: (images: IFile[], index: number) => void; } -const CommentContent: FC = memo(({ comment, can_edit, onDelete, onShowImageModal }) => { - const [isEditing, setIsEditing] = useState(false); - const { current } = useShallowSelect(selectNode); +const CommentContent: FC = memo( + ({ comment, canEdit, nodeId, onDelete, onShowImageModal }) => { + const [isEditing, setIsEditing] = useState(false); - const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]); - const stopEditing = useCallback(() => setIsEditing(false), [setIsEditing]); + const startEditing = useCallback(() => setIsEditing(true), [setIsEditing]); + const stopEditing = useCallback(() => setIsEditing(false), [setIsEditing]); - const groupped = useMemo>( - () => - reduce( - (group, file) => - file.type ? assocPath([file.type], append(file, group[file.type]), group) : group, - {}, - comment.files - ), - [comment] - ); + const groupped = useMemo>( + () => + reduce( + (group, file) => + file.type ? assocPath([file.type], append(file, group[file.type]), group) : group, + {}, + comment.files + ), + [comment] + ); - const onLockClick = useCallback(() => { - onDelete(comment.id, !comment.deleted_at); - }, [comment, onDelete]); + const onLockClick = useCallback(() => { + onDelete(comment.id, !comment.deleted_at); + }, [comment, onDelete]); - const menu = useMemo( - () => can_edit && , - [can_edit, startEditing, onLockClick] - ); + const menu = useMemo( + () => canEdit && , + [canEdit, startEditing, onLockClick] + ); - const blocks = useMemo( - () => - !!comment.text.trim() - ? formatCommentText(path(['user', 'username'], comment), comment.text) - : [], - [comment] - ); + const blocks = useMemo( + () => + !!comment.text.trim() + ? formatCommentText(path(['user', 'username'], comment), comment.text) + : [], + [comment] + ); - if (isEditing) { - return ; - } + if (isEditing) { + return ; + } - return ( -
- {comment.text && ( - - {menu} + return ( +
+ {comment.text && ( + + {menu} - - {blocks.map( - (block, key) => - COMMENT_BLOCK_RENDERERS[block.type] && - createElement(COMMENT_BLOCK_RENDERERS[block.type], { block, key }) - )} + + {blocks.map( + (block, key) => + COMMENT_BLOCK_RENDERERS[block.type] && + createElement(COMMENT_BLOCK_RENDERERS[block.type], { block, key }) + )} + + +
{getPrettyDate(comment.created_at)}
+ )} -
{getPrettyDate(comment.created_at)}
-
- )} + {groupped.image && groupped.image.length > 0 && ( +
+ {menu} - {groupped.image && groupped.image.length > 0 && ( -
- {menu} +
1, + })} + > + {groupped.image.map((file, index) => ( +
onShowImageModal(groupped.image, index)}> + {file.name} +
+ ))} +
-
1 })} - > - {groupped.image.map((file, index) => ( -
onShowImageModal(groupped.image, index)}> - {file.name} +
{getPrettyDate(comment.created_at)}
+
+ )} + + {groupped.audio && groupped.audio.length > 0 && ( + + {groupped.audio.map(file => ( +
+ {menu} + + + +
{getPrettyDate(comment.created_at)}
))} -
- -
{getPrettyDate(comment.created_at)}
-
- )} - - {groupped.audio && groupped.audio.length > 0 && ( - - {groupped.audio.map(file => ( -
- {menu} - - - -
{getPrettyDate(comment.created_at)}
-
- ))} -
- )} -
- ); -}); + + )} +
+ ); + } +); export { CommentContent }; diff --git a/src/components/editors/AudioEditor/index.tsx b/src/components/editors/AudioEditor/index.tsx index 81c92931..335f651c 100644 --- a/src/components/editors/AudioEditor/index.tsx +++ b/src/components/editors/AudioEditor/index.tsx @@ -10,7 +10,7 @@ import styles from './styles.module.scss'; import { NodeEditorProps } from '~/redux/node/types'; import { useNodeImages } from '~/utils/hooks/node/useNodeImages'; import { useNodeAudios } from '~/utils/hooks/node/useNodeAudios'; -import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik'; +import { useNodeFormContext } from '~/utils/hooks/node/useNodeFormFormik'; import { useFileUploaderContext } from '~/utils/hooks/useFileUploader'; import { UploadDropzone } from '~/components/upload/UploadDropzone'; diff --git a/src/components/editors/EditorActionsPanel/index.tsx b/src/components/editors/EditorActionsPanel/index.tsx index 7f550f92..4a1f9164 100644 --- a/src/components/editors/EditorActionsPanel/index.tsx +++ b/src/components/editors/EditorActionsPanel/index.tsx @@ -1,9 +1,8 @@ import React, { FC, createElement } from 'react'; import styles from './styles.module.scss'; -import { INode } from '~/redux/types'; import { NODE_PANEL_COMPONENTS } from '~/redux/node/constants'; import { has } from 'ramda'; -import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik'; +import { useNodeFormContext } from '~/utils/hooks/node/useNodeFormFormik'; const EditorActionsPanel: FC = () => { const { values } = useNodeFormContext(); diff --git a/src/components/editors/EditorButtons/index.tsx b/src/components/editors/EditorButtons/index.tsx index 728093be..996dd2ba 100644 --- a/src/components/editors/EditorButtons/index.tsx +++ b/src/components/editors/EditorButtons/index.tsx @@ -4,7 +4,7 @@ import { Group } from '~/components/containers/Group'; import { InputText } from '~/components/input/InputText'; import { Button } from '~/components/input/Button'; import { Padder } from '~/components/containers/Padder'; -import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik'; +import { useNodeFormContext } from '~/utils/hooks/node/useNodeFormFormik'; const EditorButtons: FC = () => { const { values, handleChange, isSubmitting } = useNodeFormContext(); @@ -28,6 +28,7 @@ const EditorButtons: FC = () => { iconRight="check" color={values.is_promoted ? 'primary' : 'lab'} disabled={isSubmitting} + type="submit" />
diff --git a/src/components/editors/EditorPublicSwitch/index.tsx b/src/components/editors/EditorPublicSwitch/index.tsx index 4ba005ba..0022db25 100644 --- a/src/components/editors/EditorPublicSwitch/index.tsx +++ b/src/components/editors/EditorPublicSwitch/index.tsx @@ -3,7 +3,7 @@ import { IEditorComponentProps } from '~/redux/node/types'; import { Button } from '~/components/input/Button'; import { Icon } from '~/components/input/Icon'; import styles from './styles.module.scss'; -import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik'; +import { useNodeFormContext } from '~/utils/hooks/node/useNodeFormFormik'; interface IProps extends IEditorComponentProps {} diff --git a/src/components/editors/EditorUploadButton/index.tsx b/src/components/editors/EditorUploadButton/index.tsx index a5198d0a..a5b0849c 100644 --- a/src/components/editors/EditorUploadButton/index.tsx +++ b/src/components/editors/EditorUploadButton/index.tsx @@ -5,7 +5,7 @@ import { UPLOAD_TYPES } from '~/redux/uploads/constants'; import { IEditorComponentProps } from '~/redux/node/types'; import { useFileUploaderContext } from '~/utils/hooks/useFileUploader'; import { getFileType } from '~/utils/uploader'; -import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik'; +import { useNodeFormContext } from '~/utils/hooks/node/useNodeFormFormik'; import { Button } from '~/components/input/Button'; type IProps = IEditorComponentProps & { diff --git a/src/components/editors/EditorUploadCoverButton/index.tsx b/src/components/editors/EditorUploadCoverButton/index.tsx index 56cd55cd..f74ddfa1 100644 --- a/src/components/editors/EditorUploadCoverButton/index.tsx +++ b/src/components/editors/EditorUploadCoverButton/index.tsx @@ -12,7 +12,7 @@ import { Icon } from '~/components/input/Icon'; import { PRESETS } from '~/constants/urls'; import { IEditorComponentProps } from '~/redux/node/types'; import { useFileUploader, useFileUploaderContext } from '~/utils/hooks/useFileUploader'; -import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik'; +import { useNodeFormContext } from '~/utils/hooks/node/useNodeFormFormik'; import { getFileType } from '~/utils/uploader'; type IProps = IEditorComponentProps & {}; diff --git a/src/components/editors/TextEditor/index.tsx b/src/components/editors/TextEditor/index.tsx index cb2f9cbe..a6d7e217 100644 --- a/src/components/editors/TextEditor/index.tsx +++ b/src/components/editors/TextEditor/index.tsx @@ -4,7 +4,7 @@ import styles from './styles.module.scss'; import { Textarea } from '~/components/input/Textarea'; import { path } from 'ramda'; import { NodeEditorProps } from '~/redux/node/types'; -import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik'; +import { useNodeFormContext } from '~/utils/hooks/node/useNodeFormFormik'; type IProps = NodeEditorProps & {}; diff --git a/src/components/editors/VideoEditor/index.tsx b/src/components/editors/VideoEditor/index.tsx index 9fd60342..489046ac 100644 --- a/src/components/editors/VideoEditor/index.tsx +++ b/src/components/editors/VideoEditor/index.tsx @@ -6,7 +6,7 @@ import { InputText } from '~/components/input/InputText'; import classnames from 'classnames'; import { getYoutubeThumb } from '~/utils/dom'; import { NodeEditorProps } from '~/redux/node/types'; -import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik'; +import { useNodeFormContext } from '~/utils/hooks/node/useNodeFormFormik'; type IProps = NodeEditorProps & {}; diff --git a/src/components/node/NodeImageSwiperBlock/index.tsx b/src/components/node/NodeImageSwiperBlock/index.tsx index bcbefb9c..cc52d330 100644 --- a/src/components/node/NodeImageSwiperBlock/index.tsx +++ b/src/components/node/NodeImageSwiperBlock/index.tsx @@ -64,7 +64,6 @@ const NodeImageSwiperBlock: FC = ({ node }) => {
onOpenPhotoSwipe(0)} className={styles.image} /> diff --git a/src/containers/dialogs/EditorCreateDialog/index.tsx b/src/containers/dialogs/EditorCreateDialog/index.tsx index 6d8d1689..ab13143a 100644 --- a/src/containers/dialogs/EditorCreateDialog/index.tsx +++ b/src/containers/dialogs/EditorCreateDialog/index.tsx @@ -3,6 +3,10 @@ import { EMPTY_NODE, NODE_TYPES } from '~/redux/node/constants'; import { EditorDialog } from '~/containers/dialogs/EditorDialog'; import { useHistory, useRouteMatch } from 'react-router'; import { values } from 'ramda'; +import { INode } from '~/redux/types'; +import { apiPostNode } from '~/redux/node/api'; +import { useUpdateNode } from '~/utils/hooks/data/useUpdateNode'; +import { useCreateNode } from '~/utils/hooks/data/useCreateNode'; const EditorCreateDialog: FC = () => { const history = useHistory(); @@ -25,11 +29,21 @@ const EditorCreateDialog: FC = () => { const data = useRef({ ...EMPTY_NODE, type, is_promoted: !isInLab }); + const createNode = useCreateNode(); + + const onSubmit = useCallback( + async (node: INode) => { + await createNode(node); + goBack(); + }, + [goBack, createNode] + ); + if (!type || !isExist) { return null; } - return ; + return ; }; export { EditorCreateDialog }; diff --git a/src/containers/dialogs/EditorDialog/index.tsx b/src/containers/dialogs/EditorDialog/index.tsx index 7ba4854a..ebb67a0e 100644 --- a/src/containers/dialogs/EditorDialog/index.tsx +++ b/src/containers/dialogs/EditorDialog/index.tsx @@ -5,7 +5,7 @@ import { NODE_EDITORS } from '~/redux/node/constants'; import { BetterScrollDialog } from '../BetterScrollDialog'; import { CoverBackdrop } from '~/components/containers/CoverBackdrop'; import { prop } from 'ramda'; -import { useNodeFormFormik } from '~/utils/hooks/useNodeFormFormik'; +import { useNodeFormFormik } from '~/utils/hooks/node/useNodeFormFormik'; import { EditorButtons } from '~/components/editors/EditorButtons'; import { FileUploaderProvider, useFileUploader } from '~/utils/hooks/useFileUploader'; import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants'; @@ -15,16 +15,18 @@ import { ModalWrapper } from '~/components/dialogs/ModalWrapper'; import { useTranslatedError } from '~/utils/hooks/useTranslatedError'; import { useCloseOnEscape } from '~/utils/hooks'; import { EditorConfirmClose } from '~/components/editors/EditorConfirmClose'; +import { on } from 'cluster'; interface Props extends IDialogProps { node: INode; + onSubmit: (node: INode) => Promise; } -const EditorDialog: FC = ({ node, onRequestClose }) => { +const EditorDialog: FC = ({ node, onRequestClose, onSubmit }) => { const [isConfirmModalShown, setConfirmModalShown] = useState(false); const uploader = useFileUploader(UPLOAD_SUBJECTS.EDITOR, UPLOAD_TARGETS.NODES, node.files); - const formik = useNodeFormFormik(node, uploader, onRequestClose); + const formik = useNodeFormFormik(node, uploader, onRequestClose, onSubmit); const { values, handleSubmit, dirty, status } = formik; const component = useMemo(() => node.type && prop(node.type, NODE_EDITORS), [node.type]); @@ -71,6 +73,7 @@ const EditorDialog: FC = ({ node, onRequestClose }) => { {isConfirmModalShown && ( )} +
{createElement(component)}
diff --git a/src/containers/dialogs/EditorEditDialog/index.tsx b/src/containers/dialogs/EditorEditDialog/index.tsx index 6cd44ae6..61afe612 100644 --- a/src/containers/dialogs/EditorEditDialog/index.tsx +++ b/src/containers/dialogs/EditorEditDialog/index.tsx @@ -5,7 +5,8 @@ import { ModalWrapper } from '~/components/dialogs/ModalWrapper'; import { LoaderCircle } from '~/components/input/LoaderCircle'; import styles from './styles.module.scss'; import { useGetNode } from '~/utils/hooks/data/useGetNode'; -import { EMPTY_NODE } from '~/redux/node/constants'; +import { useUpdateNode } from '~/utils/hooks/data/useUpdateNode'; +import { INode } from '~/redux/types'; const EditorEditDialog: FC = () => { const history = useHistory(); @@ -24,6 +25,15 @@ const EditorEditDialog: FC = () => { }, [backUrl, history]); const { node, isLoading } = useGetNode(parseInt(id, 10)); + const updateNode = useUpdateNode(parseInt(id, 10)); + + const onSubmit = useCallback( + async (node: INode) => { + await updateNode(node); + goBack(); + }, + [updateNode, goBack] + ); if (isLoading || !node) { return ( @@ -35,7 +45,7 @@ const EditorEditDialog: FC = () => { ); } - return ; + return ; }; export { EditorEditDialog }; diff --git a/src/containers/node/NodeComments/index.tsx b/src/containers/node/NodeComments/index.tsx index 4cedbe28..0d129c4a 100644 --- a/src/containers/node/NodeComments/index.tsx +++ b/src/containers/node/NodeComments/index.tsx @@ -9,6 +9,7 @@ import { useGrouppedComments } from '~/utils/hooks/node/useGrouppedComments'; import { useCommentContext } from '~/utils/context/CommentContextProvider'; import { Comment } from '~/components/comment/Comment'; import { useUserContext } from '~/utils/context/UserContextProvider'; +import { useNodeContext } from '~/utils/context/NodeContextProvider'; interface IProps { order: 'ASC' | 'DESC'; @@ -16,6 +17,8 @@ interface IProps { const NodeComments: FC = memo(({ order }) => { const user = useUserContext(); + const { node } = useNodeContext(); + const { comments, count, @@ -41,12 +44,17 @@ const NodeComments: FC = memo(({ order }) => { [left, onLoadMoreComments] ); + if (!node?.id) { + return null; + } + return (
{order === 'DESC' && more} {groupped.map(group => ( & {}; @@ -20,14 +21,8 @@ const NodePage: FC = ({ params: { id }, }, }) => { - const { - node, - isLoading, - isLoadingComments, - comments, - commentsCount, - lastSeenCurrent, - } = useFullNode(id); + const { node, isLoading } = useGetNode(parseInt(id, 10)); + const { isLoadingComments, comments, commentsCount, lastSeenCurrent } = useFullNode(id); const onShowImageModal = useImageModal(); const { onLoadMoreComments, onDelete: onDeleteComment } = useNodeComments(parseInt(id, 10)); @@ -39,6 +34,11 @@ const NodePage: FC = ({ useScrollToTop([id, isLoadingComments]); + if (!node) { + // TODO: do something here + return null; + } + return ( diff --git a/src/redux/node/actions.ts b/src/redux/node/actions.ts index b28e8cd9..a8c95483 100644 --- a/src/redux/node/actions.ts +++ b/src/redux/node/actions.ts @@ -47,16 +47,7 @@ export const nodePostLocalComment = ( nodeId, comment, callback, - type: NODE_ACTIONS.POST_COMMENT, -}); - -export const nodeSubmitLocal = ( - node: INode, - callback: (e?: string, errors?: Record) => void -) => ({ - node, - callback, - type: NODE_ACTIONS.SUBMIT_LOCAL, + type: NODE_ACTIONS.POST_LOCAL_COMMENT, }); export const nodeSetSendingComment = (is_sending_comment: boolean) => ({ @@ -69,34 +60,6 @@ export const nodeSetComments = (comments: IComment[]) => ({ type: NODE_ACTIONS.SET_COMMENTS, }); -export const nodeUpdateTags = (id: INode['id'], tags: string[]) => ({ - type: NODE_ACTIONS.UPDATE_TAGS, - id, - tags, -}); - -export const nodeDeleteTag = (id: INode['id'], tagId: ITag['ID']) => ({ - type: NODE_ACTIONS.DELETE_TAG, - id: id!, - tagId, -}); - -export const nodeSetTags = (tags: ITag[]) => ({ - type: NODE_ACTIONS.SET_TAGS, - tags, -}); - -export const nodeCreate = (node_type: INode['type'], isLab?: boolean) => ({ - type: NODE_ACTIONS.CREATE, - node_type, - isLab, -}); - -export const nodeEdit = (id: INode['id']) => ({ - type: NODE_ACTIONS.EDIT, - id, -}); - export const nodeLike = (id: INode['id']) => ({ type: NODE_ACTIONS.LIKE, id, @@ -113,17 +76,13 @@ export const nodeLock = (id: INode['id'], is_locked: boolean) => ({ is_locked, }); -export const nodeLockComment = (id: IComment['id'], is_locked: boolean) => ({ +export const nodeLockComment = (id: number, is_locked: boolean, nodeId: number) => ({ type: NODE_ACTIONS.LOCK_COMMENT, + nodeId, id, is_locked, }); -export const nodeEditComment = (id: IComment['id']) => ({ - type: NODE_ACTIONS.EDIT_COMMENT, - id, -}); - export const nodeSetEditor = (editor: INode) => ({ type: NODE_ACTIONS.SET_EDITOR, editor, diff --git a/src/redux/node/api.ts b/src/redux/node/api.ts index 8aab83d0..c2bb458c 100644 --- a/src/redux/node/api.ts +++ b/src/redux/node/api.ts @@ -42,9 +42,6 @@ export type ApiGetNodeCommentsResponse = { comments: IComment[]; comment_count: export const apiPostNode = ({ node }: ApiPostNodeRequest) => api.post(API.NODE.SAVE, node).then(cleanResult); -export const apiPostNodeLocal = ({ node }: ApiPostNodeRequest) => - api.post(API.NODE.SAVE, node).then(cleanResult); - export const getNodeDiff = ({ start, end, @@ -69,7 +66,10 @@ export const getNodeDiff = ({ .then(cleanResult); export const apiGetNode = ({ id }: ApiGetNodeRequest, config?: AxiosRequestConfig) => - api.get(API.NODE.GET_NODE(id), config).then(cleanResult); + api + .get(API.NODE.GET_NODE(id), config) + .then(cleanResult) + .then(data => ({ node: data.node, last_seen: data.last_seen })); export const apiGetNodeWithCancel = ({ id }: ApiGetNodeRequest) => { const cancelToken = axios.CancelToken.source(); diff --git a/src/redux/node/constants.ts b/src/redux/node/constants.ts index 950a5eaf..314d7ad1 100644 --- a/src/redux/node/constants.ts +++ b/src/redux/node/constants.ts @@ -22,22 +22,18 @@ import { LabPad } from '~/components/lab/LabPad'; import { LabDescription } from '~/components/lab/LabDescription'; import { LabVideo } from '~/components/lab/LabVideo'; import { LabAudio } from '~/components/lab/LabAudioBlock'; -import { LabLine } from '~/components/lab/LabLine'; const prefix = 'NODE.'; export const NODE_ACTIONS = { - SUBMIT_LOCAL: `${prefix}SUBMIT_LOCAL`, LOAD_NODE: `${prefix}LOAD_NODE`, GOTO_NODE: `${prefix}GOTO_NODE`, SET: `${prefix}SET`, - EDIT: `${prefix}EDIT`, LIKE: `${prefix}LIKE`, STAR: `${prefix}STAR`, LOCK: `${prefix}LOCK`, LOCK_COMMENT: `${prefix}LOCK_COMMENT`, EDIT_COMMENT: `${prefix}EDIT_COMMENT`, - CREATE: `${prefix}CREATE`, LOAD_MORE_COMMENTS: `${prefix}LOAD_MORE_COMMENTS`, SET_SAVE_ERRORS: `${prefix}SET_SAVE_ERRORS`, @@ -47,13 +43,10 @@ export const NODE_ACTIONS = { SET_CURRENT: `${prefix}SET_CURRENT`, SET_EDITOR: `${prefix}SET_EDITOR`, - POST_COMMENT: `${prefix}POST_LOCAL_COMMENT`, + POST_LOCAL_COMMENT: `${prefix}POST_LOCAL_COMMENT`, SET_COMMENTS: `${prefix}SET_COMMENTS`, SET_RELATED: `${prefix}SET_RELATED`, - UPDATE_TAGS: `${prefix}UPDATE_TAGS`, - DELETE_TAG: `${prefix}DELETE_TAG`, - SET_TAGS: `${prefix}SET_TAGS`, SET_COVER_IMAGE: `${prefix}SET_COVER_IMAGE`, }; diff --git a/src/redux/node/handlers.ts b/src/redux/node/handlers.ts index 2bdd2da3..87b6d9dd 100644 --- a/src/redux/node/handlers.ts +++ b/src/redux/node/handlers.ts @@ -10,7 +10,6 @@ import { nodeSetLoadingComments, nodeSetSaveErrors, nodeSetSendingComment, - nodeSetTags, } from './actions'; import { INodeState } from './reducer'; @@ -41,9 +40,6 @@ const setSendingComment = ( const setComments = (state: INodeState, { comments }: ReturnType) => assocPath(['comments'], comments, state); -const setTags = (state: INodeState, { tags }: ReturnType) => - assocPath(['current', 'tags'], tags, state); - const setEditor = (state: INodeState, { editor }: ReturnType) => assocPath(['editor'], editor, state); @@ -60,7 +56,6 @@ export const NODE_HANDLERS = { [NODE_ACTIONS.SET_CURRENT]: setCurrent, [NODE_ACTIONS.SET_SENDING_COMMENT]: setSendingComment, [NODE_ACTIONS.SET_COMMENTS]: setComments, - [NODE_ACTIONS.SET_TAGS]: setTags, [NODE_ACTIONS.SET_EDITOR]: setEditor, [NODE_ACTIONS.SET_COVER_IMAGE]: setCoverImage, }; diff --git a/src/redux/node/sagas.ts b/src/redux/node/sagas.ts index f366d1aa..df713aa0 100644 --- a/src/redux/node/sagas.ts +++ b/src/redux/node/sagas.ts @@ -1,11 +1,7 @@ import { call, put, select, takeLatest, takeLeading } from 'redux-saga/effects'; -import { push } from 'connected-react-router'; import { COMMENTS_DISPLAY, EMPTY_NODE, NODE_ACTIONS, NODE_EDITOR_DATA } from './constants'; import { - nodeCreate, - nodeDeleteTag, - nodeEdit, nodeGotoNode, nodeLike, nodeLoadNode, @@ -15,21 +11,13 @@ import { nodeSet, nodeSetComments, nodeSetCurrent, - nodeSetEditor, - nodeSetLoading, nodeSetLoadingComments, - nodeSetTags, - nodeSubmitLocal, - nodeUpdateTags, } from './actions'; import { - apiDeleteNodeTag, - apiGetNode, apiGetNodeComments, apiLockComment, apiLockNode, apiPostComment, - apiPostNode, apiPostNodeHeroic, apiPostNodeLike, apiPostNodeTags, @@ -45,6 +33,8 @@ import { DIALOGS } from '~/redux/modal/constants'; import { has } from 'ramda'; import { selectLabListNodes } from '~/redux/lab/selectors'; import { labSetList } from '~/redux/lab/actions'; +import { apiPostNode } from '~/redux/node/api'; +import { showErrorToast } from '~/utils/errors/showToast'; export function* updateNodeEverywhere(node) { const { @@ -66,42 +56,6 @@ export function* updateNodeEverywhere(node) { ); } -function* onNodeSubmitLocal({ node, callback }: ReturnType) { - try { - const { errors, node: result }: Unwrap = yield call(apiPostNode, { node }); - - if (errors && Object.values(errors).length > 0) { - callback('', errors); - return; - } - - if (node.is_promoted) { - const nodes: ReturnType = yield select(selectFlowNodes); - const updated_flow_nodes = node.id - ? nodes.map(item => (item.id === result.id ? result : item)) - : [result, ...nodes]; - yield put(flowSetNodes(updated_flow_nodes)); - } else { - const nodes: ReturnType = yield select(selectLabListNodes); - const updated_lab_nodes = node.id - ? nodes.map(item => (item.node.id === result.id ? { ...item, node: result } : item)) - : [{ node: result, comment_count: 0, last_seen: node.created_at }, ...nodes]; - yield put(labSetList({ nodes: updated_lab_nodes })); - } - - const { current } = yield select(selectNode); - - if (node.id && current.id === result.id) { - yield put(nodeSetCurrent(result)); - } - - callback(); - return; - } catch (error) { - callback(error.message); - } -} - function* onNodeGoto({ id, node_type }: ReturnType) { if (!id) { return; @@ -164,22 +118,9 @@ function* nodeGetComments(id: INode['id']) { } function* onNodeLoad({ id }: ReturnType) { - // Get node body - try { - yield put(nodeSetLoading(true)); - yield put(nodeSetLoadingComments(true)); - - const { node, last_seen }: Unwrap = yield call(apiGetNode, { id }); - - yield put(nodeSet({ current: node, lastSeenCurrent: last_seen })); - yield put(nodeSetLoading(false)); - } catch (error) { - yield put(push(URLS.ERRORS.NOT_FOUND)); - yield put(nodeSetLoading(false)); - } - // Comments try { + yield put(nodeSetLoadingComments(true)); yield call(nodeGetComments, id); yield put( @@ -197,82 +138,24 @@ function* onPostComment({ nodeId, comment, callback }: ReturnType = yield select(selectNode); + const { comments }: ReturnType = yield select(selectNode); - if (current?.id === nodeId) { - const { comments }: ReturnType = 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(); + 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* onUpdateTags({ id, tags }: ReturnType) { - try { - const { node }: Unwrap = yield call(apiPostNodeTags, { id, tags }); - const { current }: ReturnType = yield select(selectNode); - if (!node || !node.id || node.id !== current.id) return; - yield put(nodeSetTags(node.tags)); - } catch {} -} - -function* onDeleteTag({ id, tagId }: ReturnType) { - try { - const { tags }: Unwrap = yield call(apiDeleteNodeTag, { id, tagId }); - yield put(nodeSetTags(tags)); - } catch {} -} - -function* onCreateSaga({ node_type: type, isLab }: ReturnType) { - if (!type || !has(type, NODE_EDITOR_DIALOGS)) return; - - yield put( - nodeSetEditor({ - ...EMPTY_NODE, - ...(NODE_EDITOR_DATA[type] || {}), - type, - is_promoted: !isLab, - }) - ); - - yield put(modalShowDialog(NODE_EDITOR_DIALOGS[type])); -} - -function* onEditSaga({ id }: ReturnType) { - try { - if (!id) { - return; - } - - yield put(modalShowDialog(DIALOGS.LOADING)); - - const { node }: Unwrap = yield call(apiGetNode, { id }); - - if (!node.type || !has(node.type, NODE_EDITOR_DIALOGS)) return; - - if (!NODE_EDITOR_DIALOGS[node?.type]) { - throw new Error('Unknown node type'); - } - - yield put(nodeSetEditor(node)); - yield put(modalShowDialog(NODE_EDITOR_DIALOGS[node.type])); - } catch (error) { - yield put(modalSetShown(false)); - } -} - function* onLikeSaga({ id }: ReturnType) { const { current }: ReturnType = yield select(selectNode); @@ -330,22 +213,12 @@ function* onLockSaga({ id, is_locked }: ReturnType) { } } -function* onLockCommentSaga({ id, is_locked }: ReturnType) { - const { current, comments }: ReturnType = yield select(selectNode); +function* onLockCommentSaga({ nodeId, id, is_locked }: ReturnType) { + const { comments }: ReturnType = yield select(selectNode); try { - yield put( - nodeSetComments( - comments.map(comment => - comment.id === id - ? { ...comment, deleted_at: is_locked ? new Date().toISOString() : undefined } - : comment - ) - ) - ); - const data: Unwrap = yield call(apiLockComment, { - current: current.id, + current: nodeId, id, is_locked, }); @@ -357,26 +230,15 @@ function* onLockCommentSaga({ id, is_locked }: ReturnType - comment.id === id ? { ...comment, deleted_at: current.deleted_at } : comment - ) - ) - ); + } catch (e) { + showErrorToast(e); } } export default function* nodeSaga() { - yield takeLatest(NODE_ACTIONS.SUBMIT_LOCAL, onNodeSubmitLocal); yield takeLatest(NODE_ACTIONS.GOTO_NODE, onNodeGoto); yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad); - yield takeLatest(NODE_ACTIONS.POST_COMMENT, onPostComment); - yield takeLatest(NODE_ACTIONS.UPDATE_TAGS, onUpdateTags); - yield takeLatest(NODE_ACTIONS.DELETE_TAG, onDeleteTag); - yield takeLatest(NODE_ACTIONS.CREATE, onCreateSaga); - yield takeLatest(NODE_ACTIONS.EDIT, onEditSaga); + yield takeLatest(NODE_ACTIONS.POST_LOCAL_COMMENT, onPostComment); yield takeLatest(NODE_ACTIONS.LIKE, onLikeSaga); yield takeLatest(NODE_ACTIONS.STAR, onStarSaga); yield takeLatest(NODE_ACTIONS.LOCK, onLockSaga); diff --git a/src/utils/errors/showToast.ts b/src/utils/errors/showToast.ts new file mode 100644 index 00000000..ce357a56 --- /dev/null +++ b/src/utils/errors/showToast.ts @@ -0,0 +1,9 @@ +export const showErrorToast = (error: unknown) => { + if (!(error instanceof Error)) { + console.warn('catched strange exception', error); + return; + } + + // TODO: show toast or something + console.warn(error.message); +}; diff --git a/src/utils/hooks/data/useCreateNode.ts b/src/utils/hooks/data/useCreateNode.ts new file mode 100644 index 00000000..91f46546 --- /dev/null +++ b/src/utils/hooks/data/useCreateNode.ts @@ -0,0 +1,34 @@ +import { useCallback } from 'react'; +import { INode } from '~/redux/types'; +import { apiPostNode } from '~/redux/node/api'; +import { selectFlowNodes } from '~/redux/flow/selectors'; +import { flowSetNodes } from '~/redux/flow/actions'; +import { selectLabListNodes } from '~/redux/lab/selectors'; +import { labSetList } from '~/redux/lab/actions'; +import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; +import { useDispatch } from 'react-redux'; + +export const useCreateNode = () => { + const dispatch = useDispatch(); + const flowNodes = useShallowSelect(selectFlowNodes); + const labNodes = useShallowSelect(selectLabListNodes); + + return useCallback( + async (node: INode) => { + const result = await apiPostNode({ node }); + + // TODO: use another store here someday + if (node.is_promoted) { + const updatedNodes = [result.node, ...flowNodes]; + dispatch(flowSetNodes(updatedNodes)); + } else { + const updatedNodes = [ + { node: result.node, comment_count: 0, last_seen: node.created_at }, + ...labNodes, + ]; + dispatch(labSetList({ nodes: updatedNodes })); + } + }, + [flowNodes, labNodes, dispatch] + ); +}; diff --git a/src/utils/hooks/data/useGetNode.ts b/src/utils/hooks/data/useGetNode.ts index adb22903..0db9b865 100644 --- a/src/utils/hooks/data/useGetNode.ts +++ b/src/utils/hooks/data/useGetNode.ts @@ -1,19 +1,29 @@ -import { INode } from '~/redux/types'; import useSWR from 'swr'; -import { AxiosResponse } from 'axios'; import { ApiGetNodeResponse } from '~/redux/node/types'; import { API } from '~/constants/api'; -import { api } from '~/utils/api'; +import { useOnNodeSeen } from '~/utils/hooks/node/useOnNodeSeen'; +import { apiGetNode } from '~/redux/node/api'; +import { useCallback } from 'react'; +import { INode } from '~/redux/types'; -export const useGetNode = (id?: INode['id']) => { - const { data, isValidating: isLoading } = useSWR>( - API.NODE.GET_NODE(id || ''), - api.get +export const useGetNode = (id: number) => { + const { data, isValidating, mutate } = useSWR(API.NODE.GET_NODE(id), () => + apiGetNode({ id }) ); - if (!id) { - return { node: undefined, isLoading: false }; - } + const update = useCallback( + async (node?: Partial) => { + if (!data?.node) { + await mutate(); + return; + } - return { node: data?.data.node, isLoading }; + await mutate({ node: { ...data.node, ...node } }, true); + }, + [data, mutate] + ); + + useOnNodeSeen(data?.node); + + return { node: data?.node, isLoading: isValidating && !data, update }; }; diff --git a/src/utils/hooks/data/useUpdateNode.ts b/src/utils/hooks/data/useUpdateNode.ts new file mode 100644 index 00000000..6a9a8277 --- /dev/null +++ b/src/utils/hooks/data/useUpdateNode.ts @@ -0,0 +1,43 @@ +import { useGetNode } from '~/utils/hooks/data/useGetNode'; +import { useCallback } from 'react'; +import { INode } from '~/redux/types'; +import { apiPostNode } from '~/redux/node/api'; +import { selectFlowNodes } from '~/redux/flow/selectors'; +import { flowSetNodes } from '~/redux/flow/actions'; +import { selectLabListNodes } from '~/redux/lab/selectors'; +import { labSetList } from '~/redux/lab/actions'; +import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; +import { useDispatch } from 'react-redux'; + +export const useUpdateNode = (id: number) => { + const dispatch = useDispatch(); + const { update } = useGetNode(id); + const flowNodes = useShallowSelect(selectFlowNodes); + const labNodes = useShallowSelect(selectLabListNodes); + + return useCallback( + async (node: INode) => { + const result = await apiPostNode({ node }); + + if (!update) { + return; + } + + await update(result.node); + + // TODO: use another store here someday + if (node.is_promoted) { + const updatedNodes = flowNodes.map(item => + item.id === result.node.id ? result.node : item + ); + dispatch(flowSetNodes(updatedNodes)); + } else { + const updatedNodes = labNodes.map(item => + item.node.id === result.node.id ? { ...item, node: result.node } : item + ); + dispatch(labSetList({ nodes: updatedNodes })); + } + }, + [update, flowNodes, dispatch, labNodes] + ); +}; diff --git a/src/utils/hooks/node/useFullNode.ts b/src/utils/hooks/node/useFullNode.ts index 2448e608..8ace61f7 100644 --- a/src/utils/hooks/node/useFullNode.ts +++ b/src/utils/hooks/node/useFullNode.ts @@ -14,7 +14,7 @@ export const useFullNode = (id: string) => { } = useShallowSelect(selectNode); useLoadNode(id); - useOnNodeSeen(node); + // useOnNodeSeen(node); return { node, comments, commentsCount, lastSeenCurrent, isLoading, isLoadingComments }; }; diff --git a/src/utils/hooks/node/useLoadNode.ts b/src/utils/hooks/node/useLoadNode.ts index 03eee3d0..8fc2f6c8 100644 --- a/src/utils/hooks/node/useLoadNode.ts +++ b/src/utils/hooks/node/useLoadNode.ts @@ -1,7 +1,6 @@ import { useEffect } from 'react'; -import { nodeGotoNode, nodeSetCurrent } from '~/redux/node/actions'; +import { nodeGotoNode } from '~/redux/node/actions'; import { useDispatch } from 'react-redux'; -import { EMPTY_NODE } from '~/redux/node/constants'; // useLoadNode loads node on id change export const useLoadNode = (id: any) => { @@ -9,9 +8,5 @@ export const useLoadNode = (id: any) => { useEffect(() => { dispatch(nodeGotoNode(parseInt(id, 10), undefined)); - - return () => { - dispatch(nodeSetCurrent(EMPTY_NODE)); - }; }, [dispatch, id]); }; diff --git a/src/utils/hooks/node/useNodeActions.ts b/src/utils/hooks/node/useNodeActions.ts index 389ed94a..04be1d72 100644 --- a/src/utils/hooks/node/useNodeActions.ts +++ b/src/utils/hooks/node/useNodeActions.ts @@ -1,12 +1,21 @@ import { INode } from '~/redux/types'; import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { nodeEdit, nodeLike, nodeLock, nodeStar } from '~/redux/node/actions'; +import { nodeLike, nodeLock, nodeStar } from '~/redux/node/actions'; +import { modalShowDialog } from '~/redux/modal/actions'; +import { NODE_EDITOR_DIALOGS } from '~/constants/dialogs'; export const useNodeActions = (node: INode) => { const dispatch = useDispatch(); - const onEdit = useCallback(() => dispatch(nodeEdit(node.id)), [dispatch, node]); + const onEdit = useCallback(() => { + if (!node.type) { + return; + } + + dispatch(modalShowDialog(NODE_EDITOR_DIALOGS[node.type])); + }, [dispatch, node]); + const onLike = useCallback(() => dispatch(nodeLike(node.id)), [dispatch, node]); const onStar = useCallback(() => dispatch(nodeStar(node.id)), [dispatch, node]); const onLock = useCallback(() => dispatch(nodeLock(node.id, !node.deleted_at)), [dispatch, node]); diff --git a/src/utils/hooks/node/useNodeComments.ts b/src/utils/hooks/node/useNodeComments.ts index 97511475..5b477e62 100644 --- a/src/utils/hooks/node/useNodeComments.ts +++ b/src/utils/hooks/node/useNodeComments.ts @@ -1,16 +1,16 @@ import { useCallback } from 'react'; import { nodeLoadMoreComments, nodeLockComment } from '~/redux/node/actions'; -import { IComment, INode } from '~/redux/types'; +import { IComment } from '~/redux/types'; import { useDispatch } from 'react-redux'; -export const useNodeComments = (id: INode['id']) => { +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)), - [dispatch] + (id: IComment['id'], locked: boolean) => dispatch(nodeLockComment(id, locked, nodeId)), + [dispatch, nodeId] ); return { onLoadMoreComments, onDelete }; diff --git a/src/utils/hooks/useNodeFormFormik.ts b/src/utils/hooks/node/useNodeFormFormik.ts similarity index 52% rename from src/utils/hooks/useNodeFormFormik.ts rename to src/utils/hooks/node/useNodeFormFormik.ts index 37e3745e..43e95829 100644 --- a/src/utils/hooks/useNodeFormFormik.ts +++ b/src/utils/hooks/node/useNodeFormFormik.ts @@ -1,15 +1,14 @@ import { INode } from '~/redux/types'; import { FileUploader } from '~/utils/hooks/useFileUploader'; -import { useCallback, useEffect, useRef } from 'react'; -import { FormikHelpers, useFormik, useFormikContext } from 'formik'; +import { useCallback, useRef } from 'react'; +import { FormikConfig, FormikHelpers, useFormik, useFormikContext } from 'formik'; import { object } from 'yup'; -import { useDispatch } from 'react-redux'; -import { nodeSubmitLocal } from '~/redux/node/actions'; import { keys } from 'ramda'; +import { showErrorToast } from '~/utils/errors/showToast'; const validationSchema = object().shape({}); -const onSuccess = ({ resetForm, setStatus, setSubmitting, setErrors }: FormikHelpers) => ( +const afterSubmit = ({ resetForm, setStatus, setSubmitting, setErrors }: FormikHelpers) => ( e?: string, errors?: Record ) => { @@ -17,6 +16,7 @@ const onSuccess = ({ resetForm, setStatus, setSubmitting, setErrors }: FormikHel if (e) { setStatus(e); + showErrorToast(e); return; } @@ -33,17 +33,9 @@ const onSuccess = ({ resetForm, setStatus, setSubmitting, setErrors }: FormikHel export const useNodeFormFormik = ( values: INode, uploader: FileUploader, - stopEditing: () => void + stopEditing: () => void, + sendSaveRequest: (node: INode) => Promise ) => { - const dispatch = useDispatch(); - const onSubmit = useCallback( - (values: INode, helpers: FormikHelpers) => { - helpers.setSubmitting(true); - dispatch(nodeSubmitLocal(values, onSuccess(helpers))); - }, - [dispatch] - ); - const { current: initialValues } = useRef(values); const onReset = useCallback(() => { @@ -52,7 +44,19 @@ export const useNodeFormFormik = ( if (stopEditing) stopEditing(); }, [uploader, stopEditing]); - const formik = useFormik({ + const onSubmit = useCallback['onSubmit']>( + async (values, helpers) => { + try { + await sendSaveRequest({ ...values, files: uploader.files }); + afterSubmit(helpers)(); + } catch (error) { + afterSubmit(helpers)(error?.response?.data?.error, error?.response?.data?.errors); + } + }, + [sendSaveRequest, uploader.files] + ); + + return useFormik({ initialValues, validationSchema, onSubmit, @@ -60,17 +64,6 @@ export const useNodeFormFormik = ( initialStatus: '', validateOnChange: true, }); - - useEffect( - () => { - formik.setFieldValue('files', uploader.files); - }, - // because it breaks files logic - // eslint-disable-next-line - [uploader.files, formik.setFieldValue] - ); - - return formik; }; export const useNodeFormContext = () => useFormikContext(); diff --git a/src/utils/hooks/node/useNodeImages.ts b/src/utils/hooks/node/useNodeImages.ts index 375d6198..4f6b71d5 100644 --- a/src/utils/hooks/node/useNodeImages.ts +++ b/src/utils/hooks/node/useNodeImages.ts @@ -3,10 +3,6 @@ import { useMemo } from 'react'; import { UPLOAD_TYPES } from '~/redux/uploads/constants'; export const useNodeImages = (node: INode) => { - if (!node?.files) { - return []; - } - return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [ node.files, ]); diff --git a/src/utils/hooks/node/useNodePermissions.ts b/src/utils/hooks/node/useNodePermissions.ts index 4f93ba78..08eea085 100644 --- a/src/utils/hooks/node/useNodePermissions.ts +++ b/src/utils/hooks/node/useNodePermissions.ts @@ -4,7 +4,7 @@ import { useShallowSelect } from '~/utils/hooks/useShallowSelect'; import { selectUser } from '~/redux/auth/selectors'; import { INode } from '~/redux/types'; -export const useNodePermissions = (node: INode) => { +export const useNodePermissions = (node?: INode) => { const user = useShallowSelect(selectUser); const edit = useMemo(() => canEditNode(node, user), [node, user]); const like = useMemo(() => canLikeNode(node, user), [node, user]); diff --git a/src/utils/hooks/node/useNodeTags.ts b/src/utils/hooks/node/useNodeTags.ts index 0780360a..ff175f15 100644 --- a/src/utils/hooks/node/useNodeTags.ts +++ b/src/utils/hooks/node/useNodeTags.ts @@ -1,19 +1,24 @@ -import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router'; import { useCallback } from 'react'; -import { nodeDeleteTag, nodeUpdateTags } from '~/redux/node/actions'; -import { INode, ITag } from '~/redux/types'; +import { ITag } from '~/redux/types'; import { URLS } from '~/constants/urls'; +import { useGetNode } from '~/utils/hooks/data/useGetNode'; +import { apiDeleteNodeTag, apiPostNodeTags } from '~/redux/node/api'; -export const useNodeTags = (id: INode['id']) => { - const dispatch = useDispatch(); +export const useNodeTags = (id: number) => { + const { update } = useGetNode(id); const history = useHistory(); const onChange = useCallback( - (tags: string[]) => { - dispatch(nodeUpdateTags(id, tags)); + async (tags: string[]) => { + try { + const result = await apiPostNodeTags({ id, tags }); + await update({ tags: result.node.tags }); + } catch (error) { + console.warn(error); + } }, - [dispatch, id] + [id, update] ); const onClick = useCallback( @@ -28,10 +33,15 @@ export const useNodeTags = (id: INode['id']) => { ); const onDelete = useCallback( - (tagId: ITag['ID']) => { - dispatch(nodeDeleteTag(id, tagId)); + async (tagId: ITag['ID']) => { + try { + const result = await apiDeleteNodeTag({ id, tagId }); + await update({ tags: result.tags }); + } catch (e) { + console.warn(e); + } }, - [dispatch, id] + [id, update] ); return { onDelete, onChange, onClick }; diff --git a/src/utils/hooks/node/useOnNodeSeen.ts b/src/utils/hooks/node/useOnNodeSeen.ts index f9e87d9e..bf91647a 100644 --- a/src/utils/hooks/node/useOnNodeSeen.ts +++ b/src/utils/hooks/node/useOnNodeSeen.ts @@ -2,15 +2,22 @@ import { INode } from '~/redux/types'; import { useDispatch } from 'react-redux'; import { labSeenNode } from '~/redux/lab/actions'; import { flowSeenNode } from '~/redux/flow/actions'; +import { useEffect } from 'react'; // useOnNodeSeen updates node seen status across all needed places -export const useOnNodeSeen = (node: INode) => { +export const useOnNodeSeen = (node?: INode) => { const dispatch = useDispatch(); - // Remove node from updated - if (node.is_promoted) { - dispatch(flowSeenNode(node.id)); - } else { - dispatch(labSeenNode(node.id)); - } + useEffect(() => { + if (!node?.id) { + return; + } + + // Remove node from updated + if (node.is_promoted) { + dispatch(flowSeenNode(node.id)); + } else { + dispatch(labSeenNode(node.id)); + } + }, [dispatch, node]); }; diff --git a/src/utils/node.ts b/src/utils/node.ts index 0e2f426e..a9d2ba03 100644 --- a/src/utils/node.ts +++ b/src/utils/node.ts @@ -4,18 +4,17 @@ import { IUser } from '~/redux/auth/types'; import { path } from 'ramda'; import { NODE_TYPES } from '~/redux/node/constants'; -export const canEditNode = (node: Partial, user: Partial): boolean => +export const canEditNode = (node?: Partial, user?: Partial): boolean => path(['role'], user) === USER_ROLES.ADMIN || (path(['user', 'id'], node) && path(['user', 'id'], node) === path(['id'], user)); -export const canEditComment = (comment: Partial, user: Partial): boolean => - path(['role'], user) === USER_ROLES.ADMIN || - (path(['user', 'id'], comment) && path(['user', 'id'], comment) === path(['id'], user)); +export const canEditComment = (comment?: Partial, user?: Partial): boolean => + path(['role'], user) === USER_ROLES.ADMIN || path(['user', 'id'], comment) === path(['id'], user); -export const canLikeNode = (node: Partial, user: Partial): boolean => - path(['role'], user) && path(['role'], user) !== USER_ROLES.GUEST; +export const canLikeNode = (node?: Partial, user?: Partial): boolean => + path(['role'], user) !== USER_ROLES.GUEST; -export const canStarNode = (node: Partial, user: Partial): boolean => - (node.type === NODE_TYPES.IMAGE || node.is_promoted === false) && - path(['role'], user) && +export const canStarNode = (node?: Partial, user?: Partial): boolean => + path(['type'], node) === NODE_TYPES.IMAGE && + path(['is_promoted'], node) === false && path(['role'], user) === USER_ROLES.ADMIN;