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

Merge branch 'develop' into 23-labs

This commit is contained in:
Fedor Katurov 2021-03-05 17:31:33 +07:00
commit 44bbc4cd4c
147 changed files with 3292 additions and 2627 deletions

View file

@ -1,5 +1,5 @@
import { INode, IValidationErrors, IComment, ITag, IFile } from '../types';
import { NODE_ACTIONS, NODE_TYPES } from './constants';
import { IComment, IFile, INode, ITag, IValidationErrors } from '../types';
import { NODE_ACTIONS } from './constants';
import { INodeState } from './reducer';
export const nodeSet = (node: Partial<INodeState>) => ({
@ -17,7 +17,7 @@ export const nodeSetSaveErrors = (errors: IValidationErrors) => ({
type: NODE_ACTIONS.SET_SAVE_ERRORS,
});
export const nodeGotoNode = (id: number, node_type: INode['type']) => ({
export const nodeGotoNode = (id: INode['id'], node_type: INode['type']) => ({
id,
node_type,
type: NODE_ACTIONS.GOTO_NODE,
@ -44,17 +44,17 @@ export const nodeSetCurrent = (current: INodeState['current']) => ({
type: NODE_ACTIONS.SET_CURRENT,
});
export const nodePostComment = (id: number, is_before: boolean) => ({
id,
is_before,
export const nodePostLocalComment = (
nodeId: INode['id'],
comment: IComment,
callback: (e?: string) => void
) => ({
nodeId,
comment,
callback,
type: NODE_ACTIONS.POST_COMMENT,
});
export const nodeCancelCommentEdit = (id: number) => ({
id,
type: NODE_ACTIONS.CANCEL_COMMENT_EDIT,
});
export const nodeSetSendingComment = (is_sending_comment: boolean) => ({
is_sending_comment,
type: NODE_ACTIONS.SET_SENDING_COMMENT,

View file

@ -1,181 +1,102 @@
import { api, configWithToken, resultMiddleware, errorMiddleware } from '~/utils/api';
import { INode, IResultWithStatus, IComment } from '../types';
import { api, cleanResult, configWithToken, errorMiddleware, resultMiddleware } from '~/utils/api';
import { IComment, INode, IResultWithStatus } from '../types';
import { API } from '~/constants/api';
import { nodeUpdateTags, nodeLike, nodeStar, nodeLock, nodeLockComment } from './actions';
import { INodeState } from './reducer';
import { COMMENTS_DISPLAY } from './constants';
import {
ApiGetNodeRelatedRequest,
ApiGetNodeRelatedResult,
ApiGetNodeRequest,
ApiGetNodeResult,
ApiLockCommentRequest,
ApiLockcommentResult,
ApiLockNodeRequest,
ApiLockNodeResult,
ApiPostCommentRequest,
ApiPostCommentResult,
ApiPostNodeHeroicRequest,
ApiPostNodeHeroicResponse,
ApiPostNodeLikeRequest,
ApiPostNodeLikeResult,
ApiPostNodeTagsRequest,
ApiPostNodeTagsResult,
GetNodeDiffRequest,
GetNodeDiffResult,
} from '~/redux/node/types';
export const postNode = ({
access,
node,
}: {
access: string;
export type ApiPostNodeRequest = { node: INode };
export type ApiPostNodeResult = {
node: INode;
}): Promise<IResultWithStatus<INode>> =>
api
.post(API.NODE.SAVE, node, configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);
errors: Record<string, string>;
};
export const getNodes = ({
from = null,
access,
}: {
from?: string;
access: string;
}): Promise<IResultWithStatus<{ nodes: INode[] }>> =>
api
.get(API.NODE.GET, configWithToken(access, { params: { from } }))
.then(resultMiddleware)
.catch(errorMiddleware);
export type ApiGetNodeCommentsRequest = {
id: number;
take?: number;
skip?: number;
};
export type ApiGetNodeCommentsResponse = { comments: IComment[]; comment_count: number };
export const apiPostNode = ({ node }: ApiPostNodeRequest) =>
api.post<ApiPostNodeResult>(API.NODE.SAVE, node).then(cleanResult);
export const getNodeDiff = ({
start = null,
end = null,
start,
end,
take,
with_heroes,
with_updated,
with_recent,
with_valid,
access,
}: {
start?: string;
end?: string;
take?: number;
access: string;
with_heroes: boolean;
with_updated: boolean;
with_recent: boolean;
with_valid: boolean;
}): Promise<IResultWithStatus<{ nodes: INode[] }>> =>
}: GetNodeDiffRequest) =>
api
.get(
API.NODE.GET_DIFF,
configWithToken(access, {
params: {
start,
end,
take,
with_heroes,
with_updated,
with_recent,
with_valid,
},
})
)
.then(resultMiddleware)
.catch(errorMiddleware);
.get<GetNodeDiffResult>(API.NODE.GET_DIFF, {
params: {
start,
end,
take,
with_heroes,
with_updated,
with_recent,
with_valid,
},
})
.then(cleanResult);
export const getNode = ({
id,
access,
}: {
id: string | number;
access: string;
}): Promise<IResultWithStatus<{ nodes: INode[] }>> =>
api
.get(API.NODE.GET_NODE(id), configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);
export const apiGetNode = ({ id }: ApiGetNodeRequest) =>
api.get<ApiGetNodeResult>(API.NODE.GET_NODE(id)).then(cleanResult);
export const postNodeComment = ({
id,
data,
access,
}: {
access: string;
id: number;
data: IComment;
}): Promise<IResultWithStatus<{ comment: Comment }>> =>
api
.post(API.NODE.COMMENT(id), data, configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);
export const apiPostComment = ({ id, data }: ApiPostCommentRequest) =>
api.post<ApiPostCommentResult>(API.NODE.COMMENT(id), data).then(cleanResult);
export const getNodeComments = ({
export const apiGetNodeComments = ({
id,
access,
take = COMMENTS_DISPLAY,
skip = 0,
}: {
id: number;
access: string;
take?: number;
skip?: number;
}): Promise<IResultWithStatus<{ comments: IComment[]; comment_count: number }>> =>
}: ApiGetNodeCommentsRequest) =>
api
.get(API.NODE.COMMENT(id), configWithToken(access, { params: { take, skip } }))
.then(resultMiddleware)
.catch(errorMiddleware);
.get<ApiGetNodeCommentsResponse>(API.NODE.COMMENT(id), { params: { take, skip } })
.then(cleanResult);
export const getNodeRelated = ({
id,
access,
}: {
id: number;
access: string;
}): Promise<IResultWithStatus<{ related: INodeState['related'] }>> =>
api
.get(API.NODE.RELATED(id), configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);
export const apiGetNodeRelated = ({ id }: ApiGetNodeRelatedRequest) =>
api.get<ApiGetNodeRelatedResult>(API.NODE.RELATED(id)).then(cleanResult);
export const updateNodeTags = ({
id,
tags,
access,
}: ReturnType<typeof nodeUpdateTags> & { access: string }): Promise<IResultWithStatus<{
node: INode;
}>> =>
export const apiPostNodeTags = ({ id, tags }: ApiPostNodeTagsRequest) =>
api
.post(API.NODE.UPDATE_TAGS(id), { tags }, configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);
.post<ApiPostNodeTagsResult>(API.NODE.UPDATE_TAGS(id), { tags })
.then(cleanResult);
export const postNodeLike = ({
id,
access,
}: ReturnType<typeof nodeLike> & { access: string }): Promise<IResultWithStatus<{
is_liked: INode['is_liked'];
}>> =>
api
.post(API.NODE.POST_LIKE(id), {}, configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);
export const apiPostNodeLike = ({ id }: ApiPostNodeLikeRequest) =>
api.post<ApiPostNodeLikeResult>(API.NODE.POST_LIKE(id)).then(cleanResult);
export const postNodeStar = ({
id,
access,
}: ReturnType<typeof nodeStar> & { access: string }): Promise<IResultWithStatus<{
is_liked: INode['is_liked'];
}>> =>
api
.post(API.NODE.POST_STAR(id), {}, configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);
export const apiPostNodeHeroic = ({ id }: ApiPostNodeHeroicRequest) =>
api.post<ApiPostNodeHeroicResponse>(API.NODE.POST_HEROIC(id)).then(cleanResult);
export const postNodeLock = ({
id,
is_locked,
access,
}: ReturnType<typeof nodeLock> & { access: string }): Promise<IResultWithStatus<{
deleted_at: INode['deleted_at'];
}>> =>
export const apiLockNode = ({ id, is_locked }: ApiLockNodeRequest) =>
api
.post(API.NODE.POST_LOCK(id), { is_locked }, configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);
.post<ApiLockNodeResult>(API.NODE.POST_LOCK(id), { is_locked })
.then(cleanResult);
export const postNodeLockComment = ({
id,
is_locked,
current,
access,
}: ReturnType<typeof nodeLockComment> & {
access: string;
current: INode['id'];
}): Promise<IResultWithStatus<{ deleted_at: INode['deleted_at'] }>> =>
export const apiLockComment = ({ id, is_locked, current }: ApiLockCommentRequest) =>
api
.post(API.NODE.POST_LOCK_COMMENT(current, id), { is_locked }, configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);
.post<ApiLockcommentResult>(API.NODE.LOCK_COMMENT(current, id), { is_locked })
.then(cleanResult);

View file

@ -1,4 +1,4 @@
import { FC } from 'react';
import { FC, ReactElement } from 'react';
import { IComment, INode, ValueOf } from '../types';
import { NodeImageSlideBlock } from '~/components/node/NodeImageSlideBlock';
import { NodeTextBlock } from '~/components/node/NodeTextBlock';
@ -13,7 +13,7 @@ import { EditorImageUploadButton } from '~/components/editors/EditorImageUploadB
import { EditorAudioUploadButton } from '~/components/editors/EditorAudioUploadButton';
import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton';
import { modalShowPhotoswipe } from '../modal/actions';
import { IEditorComponentProps } from '~/redux/node/types';
import { IEditorComponentProps, NodeEditorProps } from '~/redux/node/types';
import { EditorFiller } from '~/components/editors/EditorFiller';
import { EditorPublicSwitch } from '~/components/editors/EditorPublicSwitch';
@ -30,7 +30,6 @@ export const NODE_ACTIONS = {
LOCK: `${prefix}LOCK`,
LOCK_COMMENT: `${prefix}LOCK_COMMENT`,
EDIT_COMMENT: `${prefix}EDIT_COMMENT`,
CANCEL_COMMENT_EDIT: `${prefix}CANCEL_COMMENT_EDIT`,
CREATE: `${prefix}CREATE`,
LOAD_MORE_COMMENTS: `${prefix}LOAD_MORE_COMMENTS`,
@ -42,7 +41,7 @@ export const NODE_ACTIONS = {
SET_COMMENT_DATA: `${prefix}SET_COMMENT_DATA`,
SET_EDITOR: `${prefix}SET_EDITOR`,
POST_COMMENT: `${prefix}POST_COMMENT`,
POST_COMMENT: `${prefix}POST_LOCAL_COMMENT`,
SET_COMMENTS: `${prefix}SET_COMMENTS`,
SET_RELATED: `${prefix}SET_RELATED`,
@ -52,15 +51,13 @@ export const NODE_ACTIONS = {
};
export const EMPTY_NODE: INode = {
id: null,
user: null,
id: 0,
user: undefined,
title: '',
files: [],
cover: null,
type: null,
cover: undefined,
type: undefined,
blocks: [],
tags: [],
@ -106,16 +103,16 @@ export const NODE_INLINES: INodeComponents = {
};
export const EMPTY_COMMENT: IComment = {
id: null,
id: 0,
text: '',
files: [],
temp_ids: [],
is_private: false,
user: null,
error: '',
user: undefined,
};
export const NODE_EDITORS = {
export const NODE_EDITORS: Record<
typeof NODE_TYPES[keyof typeof NODE_TYPES],
FC<NodeEditorProps>
> = {
[NODE_TYPES.IMAGE]: ImageEditor,
[NODE_TYPES.TEXT]: TextEditor,
[NODE_TYPES.VIDEO]: VideoEditor,

View file

@ -8,12 +8,12 @@ export type INodeState = Readonly<{
current: INode;
comments: IComment[];
related: {
albums: Record<string, Partial<INode[]>>;
similar: Partial<INode[]>;
albums: Record<string, INode[]>;
similar: INode[];
};
comment_data: Record<number, IComment>;
comment_count: number;
current_cover_image: IFile;
current_cover_image?: IFile;
error: string;
errors: Record<string, string>;
@ -38,14 +38,17 @@ const INITIAL_STATE: INodeState = {
},
comment_count: 0,
comments: [],
related: null,
current_cover_image: null,
related: {
albums: {},
similar: [],
},
current_cover_image: undefined,
is_loading: false,
is_loading_comments: false,
is_sending_comment: false,
error: null,
error: '',
errors: {},
};

View file

@ -1,69 +1,64 @@
import { takeLatest, call, put, select, delay, all, takeLeading } from 'redux-saga/effects';
import { all, call, put, select, takeLatest, takeLeading } from 'redux-saga/effects';
import { push } from 'connected-react-router';
import { omit } from 'ramda';
import {
NODE_ACTIONS,
EMPTY_NODE,
EMPTY_COMMENT,
NODE_EDITOR_DATA,
COMMENTS_DISPLAY,
EMPTY_COMMENT,
EMPTY_NODE,
NODE_ACTIONS,
NODE_EDITOR_DATA,
} from './constants';
import {
nodeSave,
nodeSetSaveErrors,
nodeLoadNode,
nodeSetLoading,
nodeSetCurrent,
nodeSetLoadingComments,
nodePostComment,
nodeSetSendingComment,
nodeSetComments,
nodeSetCommentData,
nodeUpdateTags,
nodeSetTags,
nodeCreate,
nodeSetEditor,
nodeEdit,
nodeLike,
nodeSetRelated,
nodeGotoNode,
nodeLike,
nodeLoadNode,
nodeLock,
nodeLockComment,
nodeEditComment,
nodePostLocalComment,
nodeSave,
nodeSet,
nodeCancelCommentEdit,
nodeSetCommentData,
nodeSetComments,
nodeSetCurrent,
nodeSetEditor,
nodeSetLoading,
nodeSetLoadingComments,
nodeSetRelated,
nodeSetSaveErrors,
nodeSetTags,
nodeUpdateTags,
} from './actions';
import {
postNode,
getNode,
postNodeComment,
getNodeComments,
updateNodeTags,
postNodeLike,
postNodeStar,
getNodeRelated,
postNodeLock,
postNodeLockComment,
apiGetNode,
apiGetNodeComments,
apiGetNodeRelated,
apiLockComment,
apiLockNode,
apiPostComment,
apiPostNode,
apiPostNodeHeroic,
apiPostNodeLike,
apiPostNodeTags,
} from './api';
import { reqWrapper } from '../auth/sagas';
import { flowSetNodes, flowSetUpdated } from '../flow/actions';
import { ERRORS } from '~/constants/errors';
import { modalSetShown, modalShowDialog } from '../modal/actions';
import { selectFlowNodes, selectFlow } from '../flow/selectors';
import { selectFlow, selectFlowNodes } from '../flow/selectors';
import { URLS } from '~/constants/urls';
import { selectNode } from './selectors';
import { IResultWithStatus, INode, Unwrap } from '../types';
import { Unwrap } from '../types';
import { NODE_EDITOR_DIALOGS } from '~/constants/dialogs';
import { DIALOGS } from '~/redux/modal/constants';
import { INodeState } from './reducer';
import { IFlowState } from '../flow/reducer';
import { has } from 'ramda';
export function* updateNodeEverywhere(node) {
const {
current: { id },
}: INodeState = yield select(selectNode);
const flow_nodes: IFlowState['nodes'] = yield select(selectFlowNodes);
}: ReturnType<typeof selectNode> = yield select(selectNode);
const flow_nodes: ReturnType<typeof selectFlowNodes> = yield select(selectFlowNodes);
if (id === node.id) {
yield put(nodeSetCurrent(node));
@ -79,278 +74,282 @@ export function* updateNodeEverywhere(node) {
}
function* onNodeSave({ node }: ReturnType<typeof nodeSave>) {
yield put(nodeSetSaveErrors({}));
try {
yield put(nodeSetSaveErrors({}));
const {
error,
data: { errors, node: result },
} = yield call(reqWrapper, postNode, { node });
const { errors, node: result }: Unwrap<typeof apiPostNode> = yield call(apiPostNode, { node });
if (errors && Object.values(errors).length > 0) {
return yield put(nodeSetSaveErrors(errors));
if (errors && Object.values(errors).length > 0) {
yield put(nodeSetSaveErrors(errors));
return;
}
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));
const { current } = yield select(selectNode);
if (node.id && current.id === result.id) {
yield put(nodeSetCurrent(result));
}
return yield put(modalSetShown(false));
} catch (error) {
yield put(nodeSetSaveErrors({ error: error.message || ERRORS.CANT_SAVE_NODE }));
}
if (error || !result || !result.id) {
return yield put(nodeSetSaveErrors({ error: error || ERRORS.CANT_SAVE_NODE }));
}
const nodes = 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));
const { current } = yield select(selectNode);
if (node.id && current.id === result.id) {
yield put(nodeSetCurrent(result));
}
return yield put(modalSetShown(false));
}
function* onNodeGoto({ id, node_type }: ReturnType<typeof nodeGotoNode>) {
if (!id) {
return;
}
if (node_type) yield put(nodeSetCurrent({ ...EMPTY_NODE, type: node_type }));
yield put(nodeLoadNode(id));
yield put(nodeSetCommentData(0, { ...EMPTY_COMMENT }));
yield put(nodeSetRelated(null));
yield put(nodeSetRelated({ albums: {}, similar: [] }));
}
function* onNodeLoadMoreComments() {
const {
current: { id },
comments,
}: ReturnType<typeof selectNode> = yield select(selectNode);
try {
const {
current: { id },
comments,
}: ReturnType<typeof selectNode> = yield select(selectNode);
const { data, error }: Unwrap<ReturnType<typeof getNodeComments>> = yield call(
reqWrapper,
getNodeComments,
{
if (!id) {
return;
}
const data: Unwrap<typeof apiGetNodeComments> = yield call(apiGetNodeComments, {
id,
take: COMMENTS_DISPLAY,
skip: comments.length,
});
const current: ReturnType<typeof selectNode> = yield select(selectNode);
if (!data || current.current.id != id) {
return;
}
);
const current: ReturnType<typeof selectNode> = yield select(selectNode);
if (!data || error || current.current.id != id) {
return;
}
yield put(
nodeSet({
comments: [...comments, ...data.comments],
comment_count: data.comment_count,
})
);
yield put(
nodeSet({
comments: [...comments, ...data.comments],
comment_count: data.comment_count,
})
);
} catch (error) {}
}
function* onNodeLoad({ id, order = 'ASC' }: ReturnType<typeof nodeLoadNode>) {
yield put(nodeSetLoading(true));
yield put(nodeSetLoadingComments(true));
function* onNodeLoad({ id }: ReturnType<typeof nodeLoadNode>) {
// Get node body
try {
yield put(nodeSetLoading(true));
yield put(nodeSetLoadingComments(true));
const {
data: { node, error },
} = yield call(reqWrapper, getNode, { id });
const { node }: Unwrap<typeof apiGetNode> = yield call(apiGetNode, { id });
if (error || !node || !node.id) {
yield put(nodeSetCurrent(node));
yield put(nodeSetLoading(false));
} catch (error) {
yield put(push(URLS.ERRORS.NOT_FOUND));
yield put(nodeSetLoading(false));
return;
}
yield put(nodeSetCurrent(node));
yield put(nodeSetLoading(false));
// Comments and related
try {
const [{ comments, comment_count }, { related }]: [
Unwrap<typeof apiGetNodeComments>,
Unwrap<typeof apiGetNodeRelated>
] = yield all([
call(apiGetNodeComments, { id, take: COMMENTS_DISPLAY, skip: 0 }),
call(apiGetNodeRelated, { id }),
]);
const {
comments: {
data: { comments, comment_count },
},
related: {
data: { related },
},
} = yield all({
comments: call(reqWrapper, getNodeComments, { id, take: COMMENTS_DISPLAY, skip: 0 }),
related: call(reqWrapper, getNodeRelated, { id }),
});
yield put(
nodeSet({
comments,
comment_count,
related,
is_loading_comments: false,
comment_data: { 0: { ...EMPTY_COMMENT } },
})
);
yield put(
nodeSet({
comments,
comment_count,
related,
is_loading_comments: false,
})
);
} catch {}
// Remove current node from recently updated
const { updated } = yield select(selectFlow);
if (updated.some(item => item.id === id)) {
yield put(flowSetUpdated(updated.filter(item => item.id !== id)));
}
return;
}
function* onPostComment({ id }: ReturnType<typeof nodePostComment>) {
const { current, comment_data } = yield select(selectNode);
function* onPostComment({ nodeId, comment, callback }: ReturnType<typeof nodePostLocalComment>) {
try {
const data: Unwrap<typeof apiPostComment> = yield call(apiPostComment, {
data: comment,
id: nodeId,
});
yield put(nodeSetSendingComment(true));
const {
data: { comment },
error,
} = yield call(reqWrapper, postNodeComment, { data: comment_data[id], id: current.id });
yield put(nodeSetSendingComment(false));
const { current }: ReturnType<typeof selectNode> = yield select(selectNode);
if (error || !comment) {
return yield put(nodeSetCommentData(id, { error }));
}
if (current?.id === nodeId) {
const { comments }: ReturnType<typeof selectNode> = yield select(selectNode);
const { current: current_node } = 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)),
})
);
}
if (current_node && current_node.id === current.id) {
const { comments, comment_data: current_comment_data } = yield select(selectNode);
if (id === 0) {
yield put(nodeSetCommentData(0, { ...EMPTY_COMMENT }));
yield put(nodeSetComments([comment, ...comments]));
} else {
yield put(
nodeSet({
comment_data: omit([id.toString()], current_comment_data),
comments: comments.map(item => (item.id === id ? comment : item)),
})
);
callback();
}
} catch (error) {
return callback(error.message);
}
}
function* onCancelCommentEdit({ id }: ReturnType<typeof nodeCancelCommentEdit>) {
const { comment_data } = yield select(selectNode);
yield put(
nodeSet({
comment_data: omit([id.toString()], comment_data),
})
);
}
function* onUpdateTags({ id, tags }: ReturnType<typeof nodeUpdateTags>) {
yield delay(100);
const {
data: { node },
}: IResultWithStatus<{ node: INode }> = yield call(reqWrapper, updateNodeTags, { id, tags });
const { current } = yield select(selectNode);
if (!node || !node.id || node.id !== current.id) return;
yield put(nodeSetTags(node.tags));
try {
const { node }: Unwrap<typeof apiPostNodeTags> = yield call(apiPostNodeTags, { id, tags });
const { current }: ReturnType<typeof selectNode> = yield select(selectNode);
if (!node || !node.id || node.id !== current.id) return;
yield put(nodeSetTags(node.tags));
} catch {}
}
function* onCreateSaga({ node_type: type }: ReturnType<typeof nodeCreate>) {
if (!NODE_EDITOR_DIALOGS[type]) return;
if (!type || !has(type, NODE_EDITOR_DIALOGS)) return;
yield put(nodeSetEditor({ ...EMPTY_NODE, ...(NODE_EDITOR_DATA[type] || {}), type }));
yield put(modalShowDialog(NODE_EDITOR_DIALOGS[type]));
}
function* onEditSaga({ id }: ReturnType<typeof nodeEdit>) {
yield put(modalShowDialog(DIALOGS.LOADING));
try {
if (!id) {
return;
}
const {
data: { node },
error,
} = yield call(reqWrapper, getNode, { id });
yield put(modalShowDialog(DIALOGS.LOADING));
if (error || !node || !node.type || !NODE_EDITOR_DIALOGS[node.type])
return yield put(modalSetShown(false));
const { node }: Unwrap<typeof apiGetNode> = yield call(apiGetNode, { id });
yield put(nodeSetEditor(node));
yield put(modalShowDialog(NODE_EDITOR_DIALOGS[node.type]));
if (!node.type || !has(node.type, NODE_EDITOR_DIALOGS)) return;
return true;
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<typeof nodeLike>) {
const {
current,
current: { is_liked, like_count },
} = yield select(selectNode);
const { current }: ReturnType<typeof selectNode> = yield select(selectNode);
yield call(updateNodeEverywhere, {
...current,
is_liked: !is_liked,
like_count: is_liked ? Math.max(like_count - 1, 0) : like_count + 1,
});
try {
const count = current.like_count || 0;
const { data, error } = yield call(reqWrapper, postNodeLike, { id });
yield call(updateNodeEverywhere, {
...current,
is_liked: !current.is_liked,
like_count: current.is_liked ? Math.max(count - 1, 0) : count + 1,
});
if (!error || data.is_liked === !is_liked) return; // ok and matches
const data: Unwrap<typeof apiPostNodeLike> = yield call(apiPostNodeLike, { id });
yield call(updateNodeEverywhere, { ...current, is_liked, like_count });
yield call(updateNodeEverywhere, {
...current,
is_liked: data.is_liked,
like_count: data.is_liked ? count + 1 : Math.max(count - 1, 0),
});
} catch {}
}
function* onStarSaga({ id }: ReturnType<typeof nodeLike>) {
const {
current,
current: { is_heroic },
} = yield select(selectNode);
try {
const {
current,
current: { is_heroic },
} = yield select(selectNode);
yield call(updateNodeEverywhere, { ...current, is_heroic: !is_heroic });
yield call(updateNodeEverywhere, { ...current, is_heroic: !is_heroic });
const { data, error } = yield call(reqWrapper, postNodeStar, { id });
const data: Unwrap<typeof apiPostNodeHeroic> = yield call(apiPostNodeHeroic, { id });
if (!error || data.is_heroic === !is_heroic) return; // ok and matches
yield call(updateNodeEverywhere, { ...current, is_heroic });
yield call(updateNodeEverywhere, { ...current, is_heroic: data.is_heroic });
} catch {}
}
function* onLockSaga({ id, is_locked }: ReturnType<typeof nodeLock>) {
const {
current,
current: { deleted_at },
} = yield select(selectNode);
const { current }: ReturnType<typeof selectNode> = yield select(selectNode);
yield call(updateNodeEverywhere, {
...current,
deleted_at: is_locked ? new Date().toISOString() : null,
});
try {
yield call(updateNodeEverywhere, {
...current,
deleted_at: is_locked ? new Date().toISOString() : null,
});
const { error } = yield call(reqWrapper, postNodeLock, { id, is_locked });
const data: Unwrap<typeof apiLockNode> = yield call(apiLockNode, { id, is_locked });
if (error) return yield call(updateNodeEverywhere, { ...current, deleted_at });
yield call(updateNodeEverywhere, {
...current,
deleted_at: data.deleted_at || undefined,
});
} catch {
yield call(updateNodeEverywhere, { ...current, deleted_at: current.deleted_at });
}
}
function* onLockCommentSaga({ id, is_locked }: ReturnType<typeof nodeLockComment>) {
const { current, comments } = yield select(selectNode);
const { current, comments }: ReturnType<typeof selectNode> = yield select(selectNode);
yield put(
nodeSetComments(
comments.map(comment =>
comment.id === id
? { ...comment, deleted_at: is_locked ? new Date().toISOString() : null }
: comment
try {
yield put(
nodeSetComments(
comments.map(comment =>
comment.id === id
? { ...comment, deleted_at: is_locked ? new Date().toISOString() : undefined }
: comment
)
)
)
);
);
yield call(reqWrapper, postNodeLockComment, { current: current.id, id, is_locked });
}
const data: Unwrap<typeof apiLockComment> = yield call(apiLockComment, {
current: current.id,
id,
is_locked,
});
function* onEditCommentSaga({ id }: ReturnType<typeof nodeEditComment>) {
const { comments } = yield select(selectNode);
const comment = comments.find(item => item.id === id);
if (!comment) return;
yield put(nodeSetCommentData(id, { ...EMPTY_COMMENT, ...comment }));
yield put(
nodeSetComments(
comments.map(comment =>
comment.id === id ? { ...comment, deleted_at: data.deleted_at || undefined } : comment
)
)
);
} catch {
yield put(
nodeSetComments(
comments.map(comment =>
comment.id === id ? { ...comment, deleted_at: current.deleted_at } : comment
)
)
);
}
}
export default function* nodeSaga() {
@ -358,7 +357,6 @@ export default function* nodeSaga() {
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.CANCEL_COMMENT_EDIT, onCancelCommentEdit);
yield takeLatest(NODE_ACTIONS.UPDATE_TAGS, onUpdateTags);
yield takeLatest(NODE_ACTIONS.CREATE, onCreateSaga);
yield takeLatest(NODE_ACTIONS.EDIT, onEditSaga);
@ -366,6 +364,5 @@ export default function* nodeSaga() {
yield takeLatest(NODE_ACTIONS.STAR, onStarSaga);
yield takeLatest(NODE_ACTIONS.LOCK, onLockSaga);
yield takeLatest(NODE_ACTIONS.LOCK_COMMENT, onLockCommentSaga);
yield takeLatest(NODE_ACTIONS.EDIT_COMMENT, onEditCommentSaga);
yield takeLeading(NODE_ACTIONS.LOAD_MORE_COMMENTS, onNodeLoadMoreComments);
}

View file

@ -1,10 +1,5 @@
import { IState } from '../store';
import { INodeState } from './reducer';
import { IResultWithStatus, INode } from '../types';
export const selectNode = (state: IState): INodeState => state.node;
// export const catchNodeErrors = (data: IResultWithStatus<INode>): IResultWithStatus<INode> => ({
// data,
// errors: data.errors,
// })
export const selectNode = (state: IState) => state.node;
export const selectNodeComments = (state: IState) => state.node.comments;
export const selectNodeCurrent = (state: IState) => state.node.current;

View file

@ -1,4 +1,5 @@
import { INode } from '~/redux/types';
import { IComment, INode } from '~/redux/types';
import { INodeState } from '~/redux/node/reducer';
export interface IEditorComponentProps {
data: INode;
@ -6,3 +7,85 @@ export interface IEditorComponentProps {
temp: string[];
setTemp: (val: string[]) => void;
}
export type GetNodeDiffRequest = {
start?: string;
end?: string;
take?: number;
with_heroes: boolean;
with_updated: boolean;
with_recent: boolean;
with_valid: boolean;
};
export type GetNodeDiffResult = {
before?: INode[];
after?: INode[];
heroes?: INode[];
recent?: INode[];
updated?: INode[];
valid: INode['id'][];
};
export type PostCellViewRequest = {
id: INode['id'];
flow: INode['flow'];
};
export type PostCellViewResult = unknown; // TODO: update it with actual type
export type ApiGetNodeRequest = {
id: string | number;
};
export type ApiGetNodeResult = { node: INode };
export type ApiGetNodeRelatedRequest = {
id: INode['id'];
};
export type ApiGetNodeRelatedResult = {
related: INodeState['related'];
};
export type ApiPostCommentRequest = {
id: INode['id'];
data: IComment;
};
export type ApiPostCommentResult = {
comment: IComment;
};
export type ApiPostNodeTagsRequest = {
id: INode['id'];
tags: string[];
};
export type ApiPostNodeTagsResult = {
node: INode;
};
export type ApiPostNodeLikeRequest = { id: INode['id'] };
export type ApiPostNodeLikeResult = { is_liked: boolean };
export type ApiPostNodeHeroicRequest = { id: INode['id'] };
export type ApiPostNodeHeroicResponse = { is_heroic: boolean };
export type ApiLockNodeRequest = {
id: INode['id'];
is_locked: boolean;
};
export type ApiLockNodeResult = {
deleted_at: string;
};
export type ApiLockCommentRequest = {
id: IComment['id'];
current: INode['id'];
is_locked: boolean;
};
export type ApiLockcommentResult = {
deleted_at: string;
};
export type NodeEditorProps = {
data: INode;
setData: (val: INode) => void;
temp: string[];
setTemp: (val: string[]) => void;
};