diff --git a/src/components/lab/LabTags/index.tsx b/src/components/lab/LabTags/index.tsx index 29a527d6..bd17a71a 100644 --- a/src/components/lab/LabTags/index.tsx +++ b/src/components/lab/LabTags/index.tsx @@ -27,7 +27,7 @@ const LabTags: FC = ({ tags, isLoading }) => { return (
{tags.slice(0, 10).map(tag => ( - + ))}
); diff --git a/src/components/node/NodeBottomBlock/index.tsx b/src/components/node/NodeBottomBlock/index.tsx index 4caaef51..a10bd12b 100644 --- a/src/components/node/NodeBottomBlock/index.tsx +++ b/src/components/node/NodeBottomBlock/index.tsx @@ -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 = ({ node, + canEdit, isLoading, isLoadingComments, comments, @@ -66,7 +68,7 @@ const NodeBottomBlock: FC = ({
- +
diff --git a/src/components/node/NodeTags/index.tsx b/src/components/node/NodeTags/index.tsx index d5c48923..3a1c42d4 100644 --- a/src/components/node/NodeTags/index.tsx +++ b/src/components/node/NodeTags/index.tsx @@ -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) => void; + onTagDelete?: (id: ITag['ID']) => void; } -const NodeTags: FC = memo(({ is_editable, tags, onChange, onTagClick }) => { - return ( - - ); -}); +const NodeTags: FC = memo( + ({ is_editable, is_deletable, tags, onChange, onTagClick, onTagDelete }) => { + return ( + + ); + } +); export { NodeTags }; diff --git a/src/components/node/NodeTagsBlock/index.tsx b/src/components/node/NodeTagsBlock/index.tsx index e4ee877f..ef74d7cc 100644 --- a/src/components/node/NodeTagsBlock/index.tsx +++ b/src/components/node/NodeTagsBlock/index.tsx @@ -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 = ({ node, isLoading }) => { +const NodeTagsBlock: FC = ({ node, canEdit, isLoading }) => { const dispatch = useDispatch(); const history = useHistory(); const { is_user } = useUser(); @@ -35,6 +36,13 @@ const NodeTagsBlock: FC = ({ 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 = ({ node, isLoading }) => { return ( ); }; diff --git a/src/components/tags/Tag/index.tsx b/src/components/tags/Tag/index.tsx index 5045ba12..14074d6c 100644 --- a/src/components/tags/Tag/index.tsx +++ b/src/components/tags/Tag/index.tsx @@ -12,26 +12,46 @@ interface IProps { tag: Partial; size?: 'normal' | 'big'; + is_deletable?: boolean; is_hoverable?: boolean; is_editing?: boolean; onBlur?: FocusEventHandler; onClick?: (tag: Partial) => void; + onDelete?: (id: ITag['ID']) => void; } -const Tag: FC = ({ tag, is_hoverable, is_editing, size = 'normal', onBlur, onClick }) => { +const Tag: FC = ({ + 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 ( ); diff --git a/src/components/tags/TagWrapper/index.tsx b/src/components/tags/TagWrapper/index.tsx index 2eb0f28e..f2de8bb7 100644 --- a/src/components/tags/TagWrapper/index.tsx +++ b/src/components/tags/TagWrapper/index.tsx @@ -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 = ({ children, feature, size, + is_deletable, is_hoverable, is_editing, has_input, onClick, + onDelete, title = '', -}) => ( -
-
-
{title}
- {children} -
-); +}) => { + const deletable = is_deletable && !is_editing && !has_input; + + return ( +
+
+
+
{title}
+ {children} +
+ + {deletable && ( + + )} +
+ ); +}; export { TagWrapper }; diff --git a/src/components/tags/TagWrapper/styles.module.scss b/src/components/tags/TagWrapper/styles.module.scss index 65fdccc0..e18abecc 100644 --- a/src/components/tags/TagWrapper/styles.module.scss +++ b/src/components/tags/TagWrapper/styles.module.scss @@ -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); + } +} diff --git a/src/components/tags/Tags/index.tsx b/src/components/tags/Tags/index.tsx index a76c594a..6b06db52 100644 --- a/src/components/tags/Tags/index.tsx +++ b/src/components/tags/Tags/index.tsx @@ -8,12 +8,22 @@ import { separateTags } from '~/utils/tag'; type IProps = HTMLAttributes & { tags: Partial[]; + is_deletable?: boolean; is_editable?: boolean; onTagsChange?: (tags: string[]) => void; onTagClick?: (tag: Partial) => void; + onTagDelete?: (id: ITag['ID']) => void; }; -export const Tags: FC = ({ tags, is_editable, onTagsChange, onTagClick, ...props }) => { +export const Tags: FC = ({ + tags, + is_deletable, + is_editable, + onTagsChange, + onTagClick, + onTagDelete, + ...props +}) => { const [data, setData] = useState([]); const [catTags, ordinaryTags] = useMemo(() => separateTags(tags), [tags]); @@ -56,11 +66,23 @@ export const Tags: FC = ({ tags, is_editable, onTagsChange, onTagClick, return ( {catTags.map(tag => ( - + ))} {ordinaryTags.map(tag => ( - + ))} {data.map(title => ( diff --git a/src/constants/api.ts b/src/constants/api.ts index f834633c..7b61e9b7 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -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`, diff --git a/src/layouts/NodeLayout/index.tsx b/src/layouts/NodeLayout/index.tsx index c75abed8..51140207 100644 --- a/src/layouts/NodeLayout/index.tsx +++ b/src/layouts/NodeLayout/index.tsx @@ -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 = memo( useOnNodeSeen(current); const { head, block } = useNodeBlocks(current, is_loading); + const [canEdit] = useNodePermissions(current); return (
@@ -54,13 +57,14 @@ const NodeLayout: FC = memo(