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

added tag deletion interface

This commit is contained in:
Fedor Katurov 2021-09-28 15:39:29 +07:00
parent 0a75feef8d
commit 04a7b28a53
16 changed files with 195 additions and 39 deletions

View file

@ -27,7 +27,7 @@ const LabTags: FC<IProps> = ({ tags, isLoading }) => {
return (
<div className={styles.tags}>
{tags.slice(0, 10).map(tag => (
<Tag tag={tag} key={tag.id} />
<Tag tag={tag} key={tag.ID} />
))}
</div>
);

View file

@ -16,6 +16,7 @@ import { NodeAuthorBlock } from '~/components/node/NodeAuthorBlock';
interface IProps {
node: INode;
canEdit: boolean;
isLoading: boolean;
commentsOrder: 'ASC' | 'DESC';
comments: IComment[];
@ -26,6 +27,7 @@ interface IProps {
const NodeBottomBlock: FC<IProps> = ({
node,
canEdit,
isLoading,
isLoadingComments,
comments,
@ -66,7 +68,7 @@ const NodeBottomBlock: FC<IProps> = ({
<NodeAuthorBlock node={node} />
</div>
<div className={styles.left_item}>
<NodeTagsBlock node={node} isLoading={isLoading} />
<NodeTagsBlock node={node} canEdit={canEdit} isLoading={isLoading} />
</div>
<div className={styles.left_item}>
<NodeRelatedBlock isLoading={isLoading} node={node} related={related} />

View file

@ -3,16 +3,27 @@ import { ITag } from '~/redux/types';
import { Tags } from '~/components/tags/Tags';
interface IProps {
is_deletable?: boolean;
is_editable?: boolean;
tags: ITag[];
onChange?: (tags: string[]) => void;
onTagClick?: (tag: Partial<ITag>) => void;
onTagDelete?: (id: ITag['ID']) => void;
}
const NodeTags: FC<IProps> = memo(({ is_editable, tags, onChange, onTagClick }) => {
const NodeTags: FC<IProps> = memo(
({ is_editable, is_deletable, tags, onChange, onTagClick, onTagDelete }) => {
return (
<Tags tags={tags} is_editable={is_editable} onTagsChange={onChange} onTagClick={onTagClick} />
<Tags
tags={tags}
is_editable={is_editable}
onTagsChange={onChange}
onTagClick={onTagClick}
onTagDelete={onTagDelete}
is_deletable={is_deletable}
/>
);
});
}
);
export { NodeTags };

View file

@ -1,7 +1,7 @@
import React, { FC, useCallback } from 'react';
import { INode, ITag } from '~/redux/types';
import { URLS } from '~/constants/urls';
import { nodeUpdateTags } from '~/redux/node/actions';
import { nodeDeleteTag, nodeUpdateTags } from '~/redux/node/actions';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router';
import { NodeTags } from '~/components/node/NodeTags';
@ -9,10 +9,11 @@ import { useUser } from '~/utils/hooks/user/userUser';
interface IProps {
node: INode;
canEdit: boolean;
isLoading: boolean;
}
const NodeTagsBlock: FC<IProps> = ({ node, isLoading }) => {
const NodeTagsBlock: FC<IProps> = ({ node, canEdit, isLoading }) => {
const dispatch = useDispatch();
const history = useHistory();
const { is_user } = useUser();
@ -35,6 +36,13 @@ const NodeTagsBlock: FC<IProps> = ({ node, isLoading }) => {
[history, node]
);
const onTagDelete = useCallback(
(tagId: ITag['ID']) => {
dispatch(nodeDeleteTag(node.id, tagId));
},
[dispatch, node.id]
);
if (isLoading) {
return null;
}
@ -42,9 +50,11 @@ const NodeTagsBlock: FC<IProps> = ({ node, isLoading }) => {
return (
<NodeTags
is_editable={is_user}
is_deletable={canEdit}
tags={node.tags}
onChange={onTagsChange}
onTagClick={onTagClick}
onTagDelete={onTagDelete}
/>
);
};

View file

@ -12,26 +12,46 @@ interface IProps {
tag: Partial<ITag>;
size?: 'normal' | 'big';
is_deletable?: boolean;
is_hoverable?: boolean;
is_editing?: boolean;
onBlur?: FocusEventHandler<HTMLInputElement>;
onClick?: (tag: Partial<ITag>) => void;
onDelete?: (id: ITag['ID']) => void;
}
const Tag: FC<IProps> = ({ tag, is_hoverable, is_editing, size = 'normal', onBlur, onClick }) => {
const Tag: FC<IProps> = ({
tag,
is_deletable,
is_hoverable,
is_editing,
size = 'normal',
onClick,
onDelete,
}) => {
const onClickHandler = useCallback(() => {
if (!onClick) return;
onClick(tag);
}, [tag, onClick]);
const onDeleteHandler = useCallback(() => {
if (!onDelete) {
return;
}
onDelete(tag.ID!);
}, [onDelete, tag]);
return (
<TagWrapper
feature={getTagFeature(tag)}
size={size}
is_deletable={is_deletable}
is_hoverable={is_hoverable}
is_editing={is_editing}
onClick={onClick && onClickHandler}
onDelete={onDeleteHandler}
title={tag.title}
/>
);

View file

@ -1,14 +1,18 @@
import React, { FC } from 'react';
import React, { FC, useCallback, useState } from 'react';
import classNames from 'classnames';
import styles from './styles.module.scss';
import { Manager, Popper, Reference } from 'react-popper';
import { Icon } from '~/components/input/Icon';
interface IProps {
feature?: string;
size?: string;
is_deletable?: boolean;
is_hoverable?: boolean;
is_editing?: boolean;
has_input?: boolean;
onClick?: () => void;
onDelete?: () => void;
title?: string;
}
@ -16,25 +20,39 @@ const TagWrapper: FC<IProps> = ({
children,
feature,
size,
is_deletable,
is_hoverable,
is_editing,
has_input,
onClick,
onDelete,
title = '',
}) => (
}) => {
const deletable = is_deletable && !is_editing && !has_input;
return (
<div
className={classNames(styles.tag, feature, size, {
is_hoverable,
is_editing,
deletable,
input: has_input,
clickable: onClick,
})}
onClick={onClick}
>
<div className={styles.content} onClick={onClick}>
<div className={styles.hole} />
<div className={styles.title}>{title}</div>
{children}
</div>
);
{deletable && (
<button type="button" className={styles.delete} onClick={onDelete}>
<Icon icon="close" size={20} />
</button>
)}
</div>
);
};
export { TagWrapper };

View file

@ -5,21 +5,21 @@ $big: 1.2;
.tag {
@include outer_shadow();
overflow: hidden;
cursor: default;
height: $tag_height;
background: $tag_bg;
display: flex;
flex-direction: row;
align-items: center;
justify-content: stretch;
border-radius: ($tag_height / 2) 3px 3px ($tag_height / 2);
font: $font_14_semibold;
align-self: flex-start;
padding: 0 8px 0 0;
//margin: 0 $gap $gap 0;
position: relative;
z-index: 12;
&:hover {
z-index: 40;
}
&:global(.big) {
height: $tag_height * $big;
font: $font_16_semibold;
@ -98,6 +98,19 @@ $big: 1.2;
}
}
.content {
display: flex;
flex-direction: row;
align-items: center;
justify-content: stretch;
width: 100%;
transition: transform 250ms;
:hover:global(.deletable) > & {
transform: translate(-32px, 0);
}
}
.hole {
width: $tag_height;
height: $tag_height;
@ -123,3 +136,28 @@ $big: 1.2;
text-overflow: ellipsis;
}
button.delete {
@include inner_shadow;
width: 32px;
height: 100%;
z-index: 24;
background: $red;
border: none;
padding: 0;
margin: 0 0 0 -5px;
border-radius: 0;
position: absolute;
right: -32px;
top: 0;
transition: transform 250ms;
transform: translate(0, 0);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
:hover > & {
transform: translate(-32px, 0);
}
}

View file

@ -8,12 +8,22 @@ import { separateTags } from '~/utils/tag';
type IProps = HTMLAttributes<HTMLDivElement> & {
tags: Partial<ITag>[];
is_deletable?: boolean;
is_editable?: boolean;
onTagsChange?: (tags: string[]) => void;
onTagClick?: (tag: Partial<ITag>) => void;
onTagDelete?: (id: ITag['ID']) => void;
};
export const Tags: FC<IProps> = ({ tags, is_editable, onTagsChange, onTagClick, ...props }) => {
export const Tags: FC<IProps> = ({
tags,
is_deletable,
is_editable,
onTagsChange,
onTagClick,
onTagDelete,
...props
}) => {
const [data, setData] = useState<string[]>([]);
const [catTags, ordinaryTags] = useMemo(() => separateTags(tags), [tags]);
@ -56,11 +66,23 @@ export const Tags: FC<IProps> = ({ tags, is_editable, onTagsChange, onTagClick,
return (
<TagField {...props}>
{catTags.map(tag => (
<Tag key={tag.title} tag={tag} onClick={onTagClick} />
<Tag
key={tag.title}
tag={tag}
onClick={onTagClick}
is_deletable={is_deletable}
onDelete={onTagDelete}
/>
))}
{ordinaryTags.map(tag => (
<Tag key={tag.title} tag={tag} onClick={onTagClick} />
<Tag
key={tag.title}
tag={tag}
onClick={onTagClick}
is_deletable={is_deletable}
onDelete={onTagDelete}
/>
))}
{data.map(title => (

View file

@ -1,4 +1,4 @@
import { IComment, INode } from '~/redux/types';
import { IComment, INode, ITag } from '~/redux/types';
import { ISocialProvider } from '~/redux/auth/types';
export const API = {
@ -30,6 +30,7 @@ export const API = {
COMMENT: (id: INode['id']) => `/node/${id}/comment`,
RELATED: (id: INode['id']) => `/node/${id}/related`,
UPDATE_TAGS: (id: INode['id']) => `/node/${id}/tags`,
DELETE_TAG: (id: INode['id'], tagId: ITag['ID']) => `/node/${id}/tags/${tagId}`,
POST_LIKE: (id: INode['id']) => `/node/${id}/like`,
POST_HEROIC: (id: INode['id']) => `/node/${id}/heroic`,
POST_LOCK: (id: INode['id']) => `/node/${id}/lock`,

View file

@ -18,6 +18,8 @@ import { useLoadNode } from '~/utils/hooks/node/useLoadNode';
import { URLS } from '~/constants/urls';
import { EditorEditDialog } from '~/containers/dialogs/EditorEditDialog';
import { useOnNodeSeen } from '~/utils/hooks/node/useOnNodeSeen';
import { canEditNode } from '~/utils/node';
import { useNodePermissions } from '~/utils/hooks/node/useNodePermissions';
type IProps = RouteComponentProps<{ id: string }> & {};
@ -42,6 +44,7 @@ const NodeLayout: FC<IProps> = memo(
useOnNodeSeen(current);
const { head, block } = useNodeBlocks(current, is_loading);
const [canEdit] = useNodePermissions(current);
return (
<div className={styles.wrap}>
@ -54,13 +57,14 @@ const NodeLayout: FC<IProps> = memo(
<NodePanel node={current} isLoading={is_loading} />
<NodeBottomBlock
canEdit={canEdit}
node={current}
isLoadingComments={is_loading_comments}
comments={comments}
isLoading={is_loading}
commentsCount={comment_count}
commentsOrder="DESC"
related={related}
isLoadingComments={is_loading_comments}
isLoading={is_loading}
/>
<Footer />

View file

@ -86,6 +86,12 @@ export const nodeUpdateTags = (id: INode['id'], tags: string[]) => ({
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,

View file

@ -3,6 +3,8 @@ import { IComment, INode } from '../types';
import { API } from '~/constants/api';
import { COMMENTS_DISPLAY } from './constants';
import {
ApiDeleteNodeTagsRequest,
ApiDeleteNodeTagsResult,
ApiGetNodeRelatedRequest,
ApiGetNodeRelatedResult,
ApiGetNodeRequest,
@ -101,6 +103,9 @@ export const apiPostNodeTags = ({ id, tags }: ApiPostNodeTagsRequest) =>
.post<ApiPostNodeTagsResult>(API.NODE.UPDATE_TAGS(id), { tags })
.then(cleanResult);
export const apiDeleteNodeTag = ({ id, tagId }: ApiDeleteNodeTagsRequest) =>
api.delete<ApiDeleteNodeTagsResult>(API.NODE.DELETE_TAG(id, tagId)).then(cleanResult);
export const apiPostNodeLike = ({ id }: ApiPostNodeLikeRequest) =>
api.post<ApiPostNodeLikeResult>(API.NODE.POST_LIKE(id)).then(cleanResult);

View file

@ -52,6 +52,7 @@ export const NODE_ACTIONS = {
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`,
};

View file

@ -10,6 +10,7 @@ import {
} from './constants';
import {
nodeCreate,
nodeDeleteTag,
nodeEdit,
nodeGotoNode,
nodeLike,
@ -30,6 +31,7 @@ import {
nodeUpdateTags,
} from './actions';
import {
apiDeleteNodeTag,
apiGetNode,
apiGetNodeComments,
apiGetNodeRelated,
@ -246,6 +248,13 @@ function* onUpdateTags({ id, tags }: ReturnType<typeof nodeUpdateTags>) {
} catch {}
}
function* onDeleteTag({ id, tagId }: ReturnType<typeof nodeDeleteTag>) {
try {
const { tags }: Unwrap<typeof apiDeleteNodeTag> = yield call(apiDeleteNodeTag, { id, tagId });
yield put(nodeSetTags(tags));
} catch {}
}
function* onCreateSaga({ node_type: type, isLab }: ReturnType<typeof nodeCreate>) {
if (!type || !has(type, NODE_EDITOR_DIALOGS)) return;
@ -385,6 +394,7 @@ export default function* nodeSaga() {
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.LIKE, onLikeSaga);

View file

@ -1,4 +1,4 @@
import { IComment, INode } from '~/redux/types';
import { IComment, INode, ITag } from '~/redux/types';
import { INodeState } from '~/redux/node/reducer';
export interface IEditorComponentProps {}
@ -56,6 +56,14 @@ export type ApiPostNodeTagsResult = {
node: INode;
};
export type ApiDeleteNodeTagsRequest = {
id: INode['id'];
tagId: ITag['ID'];
};
export type ApiDeleteNodeTagsResult = {
tags: ITag[];
};
export type ApiPostNodeLikeRequest = { id: INode['id'] };
export type ApiPostNodeLikeResult = { is_liked: boolean };

View file

@ -6,7 +6,7 @@ import { CallEffect } from 'redux-saga/effects';
import { AxiosResponse } from 'axios';
export interface ITag {
id: number;
ID: number;
title: string;
data: Record<string, string>;