mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
99: made node use SWR
This commit is contained in:
parent
832386d39a
commit
a1dfcc6048
27 changed files with 217 additions and 147 deletions
|
@ -10,7 +10,7 @@ import styles from './styles.module.scss';
|
||||||
import { NodeEditorProps } from '~/redux/node/types';
|
import { NodeEditorProps } from '~/redux/node/types';
|
||||||
import { useNodeImages } from '~/utils/hooks/node/useNodeImages';
|
import { useNodeImages } from '~/utils/hooks/node/useNodeImages';
|
||||||
import { useNodeAudios } from '~/utils/hooks/node/useNodeAudios';
|
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 { useFileUploaderContext } from '~/utils/hooks/useFileUploader';
|
||||||
import { UploadDropzone } from '~/components/upload/UploadDropzone';
|
import { UploadDropzone } from '~/components/upload/UploadDropzone';
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import React, { FC, createElement } from 'react';
|
import React, { FC, createElement } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { INode } from '~/redux/types';
|
|
||||||
import { NODE_PANEL_COMPONENTS } from '~/redux/node/constants';
|
import { NODE_PANEL_COMPONENTS } from '~/redux/node/constants';
|
||||||
import { has } from 'ramda';
|
import { has } from 'ramda';
|
||||||
import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik';
|
import { useNodeFormContext } from '~/utils/hooks/node/useNodeFormFormik';
|
||||||
|
|
||||||
const EditorActionsPanel: FC = () => {
|
const EditorActionsPanel: FC = () => {
|
||||||
const { values } = useNodeFormContext();
|
const { values } = useNodeFormContext();
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Group } from '~/components/containers/Group';
|
||||||
import { InputText } from '~/components/input/InputText';
|
import { InputText } from '~/components/input/InputText';
|
||||||
import { Button } from '~/components/input/Button';
|
import { Button } from '~/components/input/Button';
|
||||||
import { Padder } from '~/components/containers/Padder';
|
import { Padder } from '~/components/containers/Padder';
|
||||||
import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik';
|
import { useNodeFormContext } from '~/utils/hooks/node/useNodeFormFormik';
|
||||||
|
|
||||||
const EditorButtons: FC = () => {
|
const EditorButtons: FC = () => {
|
||||||
const { values, handleChange, isSubmitting } = useNodeFormContext();
|
const { values, handleChange, isSubmitting } = useNodeFormContext();
|
||||||
|
@ -28,6 +28,7 @@ const EditorButtons: FC = () => {
|
||||||
iconRight="check"
|
iconRight="check"
|
||||||
color={values.is_promoted ? 'primary' : 'lab'}
|
color={values.is_promoted ? 'primary' : 'lab'}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
type="submit"
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</Padder>
|
</Padder>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { IEditorComponentProps } from '~/redux/node/types';
|
||||||
import { Button } from '~/components/input/Button';
|
import { Button } from '~/components/input/Button';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik';
|
import { useNodeFormContext } from '~/utils/hooks/node/useNodeFormFormik';
|
||||||
|
|
||||||
interface IProps extends IEditorComponentProps {}
|
interface IProps extends IEditorComponentProps {}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
import { IEditorComponentProps } from '~/redux/node/types';
|
import { IEditorComponentProps } from '~/redux/node/types';
|
||||||
import { useFileUploaderContext } from '~/utils/hooks/useFileUploader';
|
import { useFileUploaderContext } from '~/utils/hooks/useFileUploader';
|
||||||
import { getFileType } from '~/utils/uploader';
|
import { getFileType } from '~/utils/uploader';
|
||||||
import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik';
|
import { useNodeFormContext } from '~/utils/hooks/node/useNodeFormFormik';
|
||||||
import { Button } from '~/components/input/Button';
|
import { Button } from '~/components/input/Button';
|
||||||
|
|
||||||
type IProps = IEditorComponentProps & {
|
type IProps = IEditorComponentProps & {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { Icon } from '~/components/input/Icon';
|
||||||
import { PRESETS } from '~/constants/urls';
|
import { PRESETS } from '~/constants/urls';
|
||||||
import { IEditorComponentProps } from '~/redux/node/types';
|
import { IEditorComponentProps } from '~/redux/node/types';
|
||||||
import { useFileUploader, useFileUploaderContext } from '~/utils/hooks/useFileUploader';
|
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';
|
import { getFileType } from '~/utils/uploader';
|
||||||
|
|
||||||
type IProps = IEditorComponentProps & {};
|
type IProps = IEditorComponentProps & {};
|
||||||
|
|
|
@ -4,7 +4,7 @@ import styles from './styles.module.scss';
|
||||||
import { Textarea } from '~/components/input/Textarea';
|
import { Textarea } from '~/components/input/Textarea';
|
||||||
import { path } from 'ramda';
|
import { path } from 'ramda';
|
||||||
import { NodeEditorProps } from '~/redux/node/types';
|
import { NodeEditorProps } from '~/redux/node/types';
|
||||||
import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik';
|
import { useNodeFormContext } from '~/utils/hooks/node/useNodeFormFormik';
|
||||||
|
|
||||||
type IProps = NodeEditorProps & {};
|
type IProps = NodeEditorProps & {};
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { InputText } from '~/components/input/InputText';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { getYoutubeThumb } from '~/utils/dom';
|
import { getYoutubeThumb } from '~/utils/dom';
|
||||||
import { NodeEditorProps } from '~/redux/node/types';
|
import { NodeEditorProps } from '~/redux/node/types';
|
||||||
import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik';
|
import { useNodeFormContext } from '~/utils/hooks/node/useNodeFormFormik';
|
||||||
|
|
||||||
type IProps = NodeEditorProps & {};
|
type IProps = NodeEditorProps & {};
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,6 @@ const NodeImageSwiperBlock: FC<IProps> = ({ node }) => {
|
||||||
<div className={styles.single}>
|
<div className={styles.single}>
|
||||||
<ImagePreloader
|
<ImagePreloader
|
||||||
file={images[0]}
|
file={images[0]}
|
||||||
onLoad={updateSwiper}
|
|
||||||
onClick={() => onOpenPhotoSwipe(0)}
|
onClick={() => onOpenPhotoSwipe(0)}
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,6 +3,10 @@ import { EMPTY_NODE, NODE_TYPES } from '~/redux/node/constants';
|
||||||
import { EditorDialog } from '~/containers/dialogs/EditorDialog';
|
import { EditorDialog } from '~/containers/dialogs/EditorDialog';
|
||||||
import { useHistory, useRouteMatch } from 'react-router';
|
import { useHistory, useRouteMatch } from 'react-router';
|
||||||
import { values } from 'ramda';
|
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 EditorCreateDialog: FC = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
@ -25,11 +29,21 @@ const EditorCreateDialog: FC = () => {
|
||||||
|
|
||||||
const data = useRef({ ...EMPTY_NODE, type, is_promoted: !isInLab });
|
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) {
|
if (!type || !isExist) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <EditorDialog node={data.current} onRequestClose={goBack} />;
|
return <EditorDialog node={data.current} onRequestClose={goBack} onSubmit={onSubmit} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { EditorCreateDialog };
|
export { EditorCreateDialog };
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { NODE_EDITORS } from '~/redux/node/constants';
|
||||||
import { BetterScrollDialog } from '../BetterScrollDialog';
|
import { BetterScrollDialog } from '../BetterScrollDialog';
|
||||||
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
|
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
|
||||||
import { prop } from 'ramda';
|
import { prop } from 'ramda';
|
||||||
import { useNodeFormFormik } from '~/utils/hooks/useNodeFormFormik';
|
import { useNodeFormFormik } from '~/utils/hooks/node/useNodeFormFormik';
|
||||||
import { EditorButtons } from '~/components/editors/EditorButtons';
|
import { EditorButtons } from '~/components/editors/EditorButtons';
|
||||||
import { FileUploaderProvider, useFileUploader } from '~/utils/hooks/useFileUploader';
|
import { FileUploaderProvider, useFileUploader } from '~/utils/hooks/useFileUploader';
|
||||||
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
|
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 { useTranslatedError } from '~/utils/hooks/useTranslatedError';
|
||||||
import { useCloseOnEscape } from '~/utils/hooks';
|
import { useCloseOnEscape } from '~/utils/hooks';
|
||||||
import { EditorConfirmClose } from '~/components/editors/EditorConfirmClose';
|
import { EditorConfirmClose } from '~/components/editors/EditorConfirmClose';
|
||||||
|
import { on } from 'cluster';
|
||||||
|
|
||||||
interface Props extends IDialogProps {
|
interface Props extends IDialogProps {
|
||||||
node: INode;
|
node: INode;
|
||||||
|
onSubmit: (node: INode) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditorDialog: FC<Props> = ({ node, onRequestClose }) => {
|
const EditorDialog: FC<Props> = ({ node, onRequestClose, onSubmit }) => {
|
||||||
const [isConfirmModalShown, setConfirmModalShown] = useState(false);
|
const [isConfirmModalShown, setConfirmModalShown] = useState(false);
|
||||||
|
|
||||||
const uploader = useFileUploader(UPLOAD_SUBJECTS.EDITOR, UPLOAD_TARGETS.NODES, node.files);
|
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 { values, handleSubmit, dirty, status } = formik;
|
||||||
|
|
||||||
const component = useMemo(() => node.type && prop(node.type, NODE_EDITORS), [node.type]);
|
const component = useMemo(() => node.type && prop(node.type, NODE_EDITORS), [node.type]);
|
||||||
|
@ -71,6 +73,7 @@ const EditorDialog: FC<Props> = ({ node, onRequestClose }) => {
|
||||||
{isConfirmModalShown && (
|
{isConfirmModalShown && (
|
||||||
<EditorConfirmClose onApprove={onRequestClose} onDecline={closeConfirmModal} />
|
<EditorConfirmClose onApprove={onRequestClose} onDecline={closeConfirmModal} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.editor}>{createElement(component)}</div>
|
<div className={styles.editor}>{createElement(component)}</div>
|
||||||
</>
|
</>
|
||||||
</BetterScrollDialog>
|
</BetterScrollDialog>
|
||||||
|
|
|
@ -5,7 +5,8 @@ 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 '~/utils/hooks/data/useGetNode';
|
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 EditorEditDialog: FC = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
@ -24,6 +25,15 @@ const EditorEditDialog: FC = () => {
|
||||||
}, [backUrl, history]);
|
}, [backUrl, history]);
|
||||||
|
|
||||||
const { node, isLoading } = useGetNode(parseInt(id, 10));
|
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) {
|
if (isLoading || !node) {
|
||||||
return (
|
return (
|
||||||
|
@ -35,7 +45,7 @@ const EditorEditDialog: FC = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <EditorDialog node={node || EMPTY_NODE} onRequestClose={goBack} />;
|
return <EditorDialog node={node} onRequestClose={goBack} onSubmit={onSubmit} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { EditorEditDialog };
|
export { EditorEditDialog };
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { CommentContextProvider } from '~/utils/context/CommentContextProvider';
|
||||||
import { TagsContextProvider } from '~/utils/context/TagsContextProvider';
|
import { TagsContextProvider } from '~/utils/context/TagsContextProvider';
|
||||||
import { useNodePermissions } from '~/utils/hooks/node/useNodePermissions';
|
import { useNodePermissions } from '~/utils/hooks/node/useNodePermissions';
|
||||||
import { NodeRelatedProvider } from '~/utils/providers/NodeRelatedProvider';
|
import { NodeRelatedProvider } from '~/utils/providers/NodeRelatedProvider';
|
||||||
|
import { useGetNode } from '~/utils/hooks/data/useGetNode';
|
||||||
|
|
||||||
type Props = RouteComponentProps<{ id: string }> & {};
|
type Props = RouteComponentProps<{ id: string }> & {};
|
||||||
|
|
||||||
|
@ -20,14 +21,8 @@ const NodePage: FC<Props> = ({
|
||||||
params: { id },
|
params: { id },
|
||||||
},
|
},
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const { node, isLoading } = useGetNode(parseInt(id, 10));
|
||||||
node,
|
const { isLoadingComments, comments, commentsCount, lastSeenCurrent } = useFullNode(id);
|
||||||
isLoading,
|
|
||||||
isLoadingComments,
|
|
||||||
comments,
|
|
||||||
commentsCount,
|
|
||||||
lastSeenCurrent,
|
|
||||||
} = useFullNode(id);
|
|
||||||
|
|
||||||
const onShowImageModal = useImageModal();
|
const onShowImageModal = useImageModal();
|
||||||
const { onLoadMoreComments, onDelete: onDeleteComment } = useNodeComments(parseInt(id, 10));
|
const { onLoadMoreComments, onDelete: onDeleteComment } = useNodeComments(parseInt(id, 10));
|
||||||
|
@ -39,6 +34,11 @@ const NodePage: FC<Props> = ({
|
||||||
|
|
||||||
useScrollToTop([id, isLoadingComments]);
|
useScrollToTop([id, isLoadingComments]);
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
// TODO: do something here
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeContextProvider node={node} isLoading={isLoading}>
|
<NodeContextProvider node={node} isLoading={isLoading}>
|
||||||
<NodeRelatedProvider id={parseInt(id, 10)} tags={node.tags}>
|
<NodeRelatedProvider id={parseInt(id, 10)} tags={node.tags}>
|
||||||
|
|
|
@ -50,15 +50,6 @@ export const nodePostLocalComment = (
|
||||||
type: NODE_ACTIONS.POST_COMMENT,
|
type: NODE_ACTIONS.POST_COMMENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const nodeSubmitLocal = (
|
|
||||||
node: INode,
|
|
||||||
callback: (e?: string, errors?: Record<string, string>) => void
|
|
||||||
) => ({
|
|
||||||
node,
|
|
||||||
callback,
|
|
||||||
type: NODE_ACTIONS.SUBMIT_LOCAL,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const nodeSetSendingComment = (is_sending_comment: boolean) => ({
|
export const nodeSetSendingComment = (is_sending_comment: boolean) => ({
|
||||||
is_sending_comment,
|
is_sending_comment,
|
||||||
type: NODE_ACTIONS.SET_SENDING_COMMENT,
|
type: NODE_ACTIONS.SET_SENDING_COMMENT,
|
||||||
|
|
|
@ -42,9 +42,6 @@ export type ApiGetNodeCommentsResponse = { comments: IComment[]; comment_count:
|
||||||
export const apiPostNode = ({ node }: ApiPostNodeRequest) =>
|
export const apiPostNode = ({ node }: ApiPostNodeRequest) =>
|
||||||
api.post<ApiPostNodeResult>(API.NODE.SAVE, node).then(cleanResult);
|
api.post<ApiPostNodeResult>(API.NODE.SAVE, node).then(cleanResult);
|
||||||
|
|
||||||
export const apiPostNodeLocal = ({ node }: ApiPostNodeRequest) =>
|
|
||||||
api.post<ApiPostNodeResult>(API.NODE.SAVE, node).then(cleanResult);
|
|
||||||
|
|
||||||
export const getNodeDiff = ({
|
export const getNodeDiff = ({
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
|
@ -69,7 +66,10 @@ export const getNodeDiff = ({
|
||||||
.then(cleanResult);
|
.then(cleanResult);
|
||||||
|
|
||||||
export const apiGetNode = ({ id }: ApiGetNodeRequest, config?: AxiosRequestConfig) =>
|
export const apiGetNode = ({ id }: ApiGetNodeRequest, config?: AxiosRequestConfig) =>
|
||||||
api.get<ApiGetNodeResponse>(API.NODE.GET_NODE(id), config).then(cleanResult);
|
api
|
||||||
|
.get<ApiGetNodeResponse>(API.NODE.GET_NODE(id), config)
|
||||||
|
.then(cleanResult)
|
||||||
|
.then(data => ({ node: data.node, last_seen: data.last_seen }));
|
||||||
|
|
||||||
export const apiGetNodeWithCancel = ({ id }: ApiGetNodeRequest) => {
|
export const apiGetNodeWithCancel = ({ id }: ApiGetNodeRequest) => {
|
||||||
const cancelToken = axios.CancelToken.source();
|
const cancelToken = axios.CancelToken.source();
|
||||||
|
|
|
@ -22,11 +22,9 @@ import { LabPad } from '~/components/lab/LabPad';
|
||||||
import { LabDescription } from '~/components/lab/LabDescription';
|
import { LabDescription } from '~/components/lab/LabDescription';
|
||||||
import { LabVideo } from '~/components/lab/LabVideo';
|
import { LabVideo } from '~/components/lab/LabVideo';
|
||||||
import { LabAudio } from '~/components/lab/LabAudioBlock';
|
import { LabAudio } from '~/components/lab/LabAudioBlock';
|
||||||
import { LabLine } from '~/components/lab/LabLine';
|
|
||||||
|
|
||||||
const prefix = 'NODE.';
|
const prefix = 'NODE.';
|
||||||
export const NODE_ACTIONS = {
|
export const NODE_ACTIONS = {
|
||||||
SUBMIT_LOCAL: `${prefix}SUBMIT_LOCAL`,
|
|
||||||
LOAD_NODE: `${prefix}LOAD_NODE`,
|
LOAD_NODE: `${prefix}LOAD_NODE`,
|
||||||
GOTO_NODE: `${prefix}GOTO_NODE`,
|
GOTO_NODE: `${prefix}GOTO_NODE`,
|
||||||
SET: `${prefix}SET`,
|
SET: `${prefix}SET`,
|
||||||
|
|
|
@ -19,7 +19,6 @@ import {
|
||||||
nodeSetLoading,
|
nodeSetLoading,
|
||||||
nodeSetLoadingComments,
|
nodeSetLoadingComments,
|
||||||
nodeSetTags,
|
nodeSetTags,
|
||||||
nodeSubmitLocal,
|
|
||||||
nodeUpdateTags,
|
nodeUpdateTags,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import {
|
import {
|
||||||
|
@ -29,7 +28,6 @@ import {
|
||||||
apiLockComment,
|
apiLockComment,
|
||||||
apiLockNode,
|
apiLockNode,
|
||||||
apiPostComment,
|
apiPostComment,
|
||||||
apiPostNode,
|
|
||||||
apiPostNodeHeroic,
|
apiPostNodeHeroic,
|
||||||
apiPostNodeLike,
|
apiPostNodeLike,
|
||||||
apiPostNodeTags,
|
apiPostNodeTags,
|
||||||
|
@ -45,6 +43,7 @@ import { DIALOGS } from '~/redux/modal/constants';
|
||||||
import { has } from 'ramda';
|
import { has } from 'ramda';
|
||||||
import { selectLabListNodes } from '~/redux/lab/selectors';
|
import { selectLabListNodes } from '~/redux/lab/selectors';
|
||||||
import { labSetList } from '~/redux/lab/actions';
|
import { labSetList } from '~/redux/lab/actions';
|
||||||
|
import { apiPostNode } from '~/redux/node/api';
|
||||||
|
|
||||||
export function* updateNodeEverywhere(node) {
|
export function* updateNodeEverywhere(node) {
|
||||||
const {
|
const {
|
||||||
|
@ -66,42 +65,6 @@ export function* updateNodeEverywhere(node) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function* onNodeSubmitLocal({ node, callback }: ReturnType<typeof nodeSubmitLocal>) {
|
|
||||||
try {
|
|
||||||
const { errors, node: result }: Unwrap<typeof apiPostNode> = yield call(apiPostNode, { node });
|
|
||||||
|
|
||||||
if (errors && Object.values(errors).length > 0) {
|
|
||||||
callback('', errors);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.is_promoted) {
|
|
||||||
const nodes: ReturnType<typeof selectFlowNodes> = 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<typeof selectLabListNodes> = 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<typeof nodeGotoNode>) {
|
function* onNodeGoto({ id, node_type }: ReturnType<typeof nodeGotoNode>) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return;
|
return;
|
||||||
|
@ -369,7 +332,6 @@ function* onLockCommentSaga({ id, is_locked }: ReturnType<typeof nodeLockComment
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function* nodeSaga() {
|
export default function* nodeSaga() {
|
||||||
yield takeLatest(NODE_ACTIONS.SUBMIT_LOCAL, onNodeSubmitLocal);
|
|
||||||
yield takeLatest(NODE_ACTIONS.GOTO_NODE, onNodeGoto);
|
yield takeLatest(NODE_ACTIONS.GOTO_NODE, onNodeGoto);
|
||||||
yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad);
|
yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad);
|
||||||
yield takeLatest(NODE_ACTIONS.POST_COMMENT, onPostComment);
|
yield takeLatest(NODE_ACTIONS.POST_COMMENT, onPostComment);
|
||||||
|
|
34
src/utils/hooks/data/useCreateNode.ts
Normal file
34
src/utils/hooks/data/useCreateNode.ts
Normal file
|
@ -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]
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,19 +1,29 @@
|
||||||
import { INode } from '~/redux/types';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { AxiosResponse } from 'axios';
|
|
||||||
import { ApiGetNodeResponse } from '~/redux/node/types';
|
import { ApiGetNodeResponse } from '~/redux/node/types';
|
||||||
import { API } from '~/constants/api';
|
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']) => {
|
export const useGetNode = (id: number) => {
|
||||||
const { data, isValidating: isLoading } = useSWR<AxiosResponse<ApiGetNodeResponse>>(
|
const { data, isValidating, mutate } = useSWR<ApiGetNodeResponse>(API.NODE.GET_NODE(id), () =>
|
||||||
API.NODE.GET_NODE(id || ''),
|
apiGetNode({ id })
|
||||||
api.get
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!id) {
|
const update = useCallback(
|
||||||
return { node: undefined, isLoading: false };
|
async (node?: Partial<INode>) => {
|
||||||
|
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 };
|
||||||
};
|
};
|
||||||
|
|
43
src/utils/hooks/data/useUpdateNode.ts
Normal file
43
src/utils/hooks/data/useUpdateNode.ts
Normal file
|
@ -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]
|
||||||
|
);
|
||||||
|
};
|
|
@ -13,8 +13,8 @@ export const useFullNode = (id: string) => {
|
||||||
lastSeenCurrent,
|
lastSeenCurrent,
|
||||||
} = useShallowSelect(selectNode);
|
} = useShallowSelect(selectNode);
|
||||||
|
|
||||||
useLoadNode(id);
|
// useLoadNode(id);
|
||||||
useOnNodeSeen(node);
|
// useOnNodeSeen(node);
|
||||||
|
|
||||||
return { node, comments, commentsCount, lastSeenCurrent, isLoading, isLoadingComments };
|
return { node, comments, commentsCount, lastSeenCurrent, isLoading, isLoadingComments };
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import { FileUploader } from '~/utils/hooks/useFileUploader';
|
import { FileUploader } from '~/utils/hooks/useFileUploader';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { FormikHelpers, useFormik, useFormikContext } from 'formik';
|
import { FormikConfig, FormikHelpers, useFormik, useFormikContext } from 'formik';
|
||||||
import { object } from 'yup';
|
import { object } from 'yup';
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { nodeSubmitLocal } from '~/redux/node/actions';
|
|
||||||
import { keys } from 'ramda';
|
import { keys } from 'ramda';
|
||||||
|
|
||||||
const validationSchema = object().shape({});
|
const validationSchema = object().shape({});
|
||||||
|
|
||||||
const onSuccess = ({ resetForm, setStatus, setSubmitting, setErrors }: FormikHelpers<INode>) => (
|
const afterSubmit = ({ resetForm, setStatus, setSubmitting, setErrors }: FormikHelpers<INode>) => (
|
||||||
e?: string,
|
e?: string,
|
||||||
errors?: Record<string, string>
|
errors?: Record<string, string>
|
||||||
) => {
|
) => {
|
||||||
|
@ -33,17 +31,9 @@ const onSuccess = ({ resetForm, setStatus, setSubmitting, setErrors }: FormikHel
|
||||||
export const useNodeFormFormik = (
|
export const useNodeFormFormik = (
|
||||||
values: INode,
|
values: INode,
|
||||||
uploader: FileUploader,
|
uploader: FileUploader,
|
||||||
stopEditing: () => void
|
stopEditing: () => void,
|
||||||
|
sendSaveRequest: (node: INode) => Promise<unknown>
|
||||||
) => {
|
) => {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const onSubmit = useCallback(
|
|
||||||
(values: INode, helpers: FormikHelpers<INode>) => {
|
|
||||||
helpers.setSubmitting(true);
|
|
||||||
dispatch(nodeSubmitLocal(values, onSuccess(helpers)));
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { current: initialValues } = useRef(values);
|
const { current: initialValues } = useRef(values);
|
||||||
|
|
||||||
const onReset = useCallback(() => {
|
const onReset = useCallback(() => {
|
||||||
|
@ -52,7 +42,19 @@ export const useNodeFormFormik = (
|
||||||
if (stopEditing) stopEditing();
|
if (stopEditing) stopEditing();
|
||||||
}, [uploader, stopEditing]);
|
}, [uploader, stopEditing]);
|
||||||
|
|
||||||
const formik = useFormik<INode>({
|
const onSubmit = useCallback<FormikConfig<INode>['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<INode>({
|
||||||
initialValues,
|
initialValues,
|
||||||
validationSchema,
|
validationSchema,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
@ -60,17 +62,6 @@ export const useNodeFormFormik = (
|
||||||
initialStatus: '',
|
initialStatus: '',
|
||||||
validateOnChange: true,
|
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<INode>();
|
export const useNodeFormContext = () => useFormikContext<INode>();
|
|
@ -3,10 +3,6 @@ import { useMemo } from 'react';
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
|
|
||||||
export const useNodeImages = (node: INode) => {
|
export const useNodeImages = (node: INode) => {
|
||||||
if (!node?.files) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [
|
return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [
|
||||||
node.files,
|
node.files,
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
import { selectUser } from '~/redux/auth/selectors';
|
import { selectUser } from '~/redux/auth/selectors';
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
|
|
||||||
export const useNodePermissions = (node: INode) => {
|
export const useNodePermissions = (node?: INode) => {
|
||||||
const user = useShallowSelect(selectUser);
|
const user = useShallowSelect(selectUser);
|
||||||
const edit = useMemo(() => canEditNode(node, user), [node, user]);
|
const edit = useMemo(() => canEditNode(node, user), [node, user]);
|
||||||
const like = useMemo(() => canLikeNode(node, user), [node, user]);
|
const like = useMemo(() => canLikeNode(node, user), [node, user]);
|
||||||
|
|
|
@ -2,18 +2,26 @@ import { useDispatch } from 'react-redux';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { nodeDeleteTag, nodeUpdateTags } from '~/redux/node/actions';
|
import { nodeDeleteTag, nodeUpdateTags } from '~/redux/node/actions';
|
||||||
import { INode, ITag } from '~/redux/types';
|
import { ITag } from '~/redux/types';
|
||||||
import { URLS } from '~/constants/urls';
|
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']) => {
|
export const useNodeTags = (id: number) => {
|
||||||
|
const { update } = useGetNode(id);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(tags: string[]) => {
|
async (tags: string[]) => {
|
||||||
dispatch(nodeUpdateTags(id, tags));
|
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(
|
const onClick = useCallback(
|
||||||
|
@ -28,10 +36,15 @@ export const useNodeTags = (id: INode['id']) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDelete = useCallback(
|
const onDelete = useCallback(
|
||||||
(tagId: ITag['ID']) => {
|
async (tagId: ITag['ID']) => {
|
||||||
dispatch(nodeDeleteTag(id, tagId));
|
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 };
|
return { onDelete, onChange, onClick };
|
||||||
|
|
|
@ -2,15 +2,22 @@ import { INode } from '~/redux/types';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { labSeenNode } from '~/redux/lab/actions';
|
import { labSeenNode } from '~/redux/lab/actions';
|
||||||
import { flowSeenNode } from '~/redux/flow/actions';
|
import { flowSeenNode } from '~/redux/flow/actions';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
// useOnNodeSeen updates node seen status across all needed places
|
// useOnNodeSeen updates node seen status across all needed places
|
||||||
export const useOnNodeSeen = (node: INode) => {
|
export const useOnNodeSeen = (node?: INode) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!node?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove node from updated
|
// Remove node from updated
|
||||||
if (node.is_promoted) {
|
if (node.is_promoted) {
|
||||||
dispatch(flowSeenNode(node.id));
|
dispatch(flowSeenNode(node.id));
|
||||||
} else {
|
} else {
|
||||||
dispatch(labSeenNode(node.id));
|
dispatch(labSeenNode(node.id));
|
||||||
}
|
}
|
||||||
|
}, [dispatch, node]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,18 +4,17 @@ import { IUser } from '~/redux/auth/types';
|
||||||
import { path } from 'ramda';
|
import { path } from 'ramda';
|
||||||
import { NODE_TYPES } from '~/redux/node/constants';
|
import { NODE_TYPES } from '~/redux/node/constants';
|
||||||
|
|
||||||
export const canEditNode = (node: Partial<INode>, user: Partial<IUser>): boolean =>
|
export const canEditNode = (node?: Partial<INode>, user?: Partial<IUser>): boolean =>
|
||||||
path(['role'], user) === USER_ROLES.ADMIN ||
|
path(['role'], user) === USER_ROLES.ADMIN ||
|
||||||
(path(['user', 'id'], node) && path(['user', 'id'], node) === path(['id'], user));
|
(path(['user', 'id'], node) && path(['user', 'id'], node) === path(['id'], user));
|
||||||
|
|
||||||
export const canEditComment = (comment: Partial<ICommentGroup>, user: Partial<IUser>): boolean =>
|
export const canEditComment = (comment?: Partial<ICommentGroup>, user?: Partial<IUser>): boolean =>
|
||||||
path(['role'], user) === USER_ROLES.ADMIN ||
|
path(['role'], user) === USER_ROLES.ADMIN || path(['user', 'id'], comment) === path(['id'], user);
|
||||||
(path(['user', 'id'], comment) && path(['user', 'id'], comment) === path(['id'], user));
|
|
||||||
|
|
||||||
export const canLikeNode = (node: Partial<INode>, user: Partial<IUser>): boolean =>
|
export const canLikeNode = (node?: Partial<INode>, user?: Partial<IUser>): boolean =>
|
||||||
path(['role'], user) && path(['role'], user) !== USER_ROLES.GUEST;
|
path(['role'], user) !== USER_ROLES.GUEST;
|
||||||
|
|
||||||
export const canStarNode = (node: Partial<INode>, user: Partial<IUser>): boolean =>
|
export const canStarNode = (node?: Partial<INode>, user?: Partial<IUser>): boolean =>
|
||||||
(node.type === NODE_TYPES.IMAGE || node.is_promoted === false) &&
|
path(['type'], node) === NODE_TYPES.IMAGE &&
|
||||||
path(['role'], user) &&
|
path(['is_promoted'], node) === false &&
|
||||||
path(['role'], user) === USER_ROLES.ADMIN;
|
path(['role'], user) === USER_ROLES.ADMIN;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue