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

99 use swr (#100)

* 99: made node use SWR

* 99: fixed comments for SWR node

* 99: added error toast to useNodeFormFormik.ts
This commit is contained in:
muerwre 2022-01-02 17:10:21 +07:00 committed by GitHub
parent 832386d39a
commit c2d1c2bfc9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 366 additions and 413 deletions

View file

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

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

View file

@ -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<AxiosResponse<ApiGetNodeResponse>>(
API.NODE.GET_NODE(id || ''),
api.get
export const useGetNode = (id: number) => {
const { data, isValidating, mutate } = useSWR<ApiGetNodeResponse>(API.NODE.GET_NODE(id), () =>
apiGetNode({ id })
);
if (!id) {
return { node: undefined, isLoading: false };
}
const update = useCallback(
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 };
};

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

View file

@ -14,7 +14,7 @@ export const useFullNode = (id: string) => {
} = useShallowSelect(selectNode);
useLoadNode(id);
useOnNodeSeen(node);
// useOnNodeSeen(node);
return { node, comments, commentsCount, lastSeenCurrent, isLoading, isLoadingComments };
};

View file

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

View file

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

View file

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

View file

@ -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<INode>) => (
const afterSubmit = ({ resetForm, setStatus, setSubmitting, setErrors }: FormikHelpers<INode>) => (
e?: string,
errors?: Record<string, string>
) => {
@ -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<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 onReset = useCallback(() => {
@ -52,7 +44,19 @@ export const useNodeFormFormik = (
if (stopEditing) 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,
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<INode>();

View file

@ -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,
]);

View file

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

View file

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

View file

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

View file

@ -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<INode>, user: Partial<IUser>): boolean =>
export const canEditNode = (node?: Partial<INode>, user?: Partial<IUser>): boolean =>
path(['role'], user) === USER_ROLES.ADMIN ||
(path(['user', 'id'], node) && path(['user', 'id'], node) === path(['id'], user));
export const canEditComment = (comment: Partial<ICommentGroup>, user: Partial<IUser>): boolean =>
path(['role'], user) === USER_ROLES.ADMIN ||
(path(['user', 'id'], comment) && path(['user', 'id'], comment) === path(['id'], user));
export const canEditComment = (comment?: Partial<ICommentGroup>, user?: Partial<IUser>): boolean =>
path(['role'], user) === USER_ROLES.ADMIN || path(['user', 'id'], comment) === path(['id'], user);
export const canLikeNode = (node: Partial<INode>, user: Partial<IUser>): boolean =>
path(['role'], user) && path(['role'], user) !== USER_ROLES.GUEST;
export const canLikeNode = (node?: Partial<INode>, user?: Partial<IUser>): boolean =>
path(['role'], user) !== USER_ROLES.GUEST;
export const canStarNode = (node: Partial<INode>, user: Partial<IUser>): boolean =>
(node.type === NODE_TYPES.IMAGE || node.is_promoted === false) &&
path(['role'], user) &&
export const canStarNode = (node?: Partial<INode>, user?: Partial<IUser>): boolean =>
path(['type'], node) === NODE_TYPES.IMAGE &&
path(['is_promoted'], node) === false &&
path(['role'], user) === USER_ROLES.ADMIN;