-
Notifications
You must be signed in to change notification settings - Fork 373
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(tag): support merge tags (#7402)
- Loading branch information
1 parent
b606f65
commit 16b361f
Showing
18 changed files
with
660 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
.sf-metadata-merge-tags-selector { | ||
left: 0; | ||
min-height: 160px; | ||
width: 300px; | ||
padding: 0; | ||
opacity: 1; | ||
overflow: hidden; | ||
position: fixed; | ||
background-color: #fff; | ||
border: 1px solid #dedede; | ||
border-radius: 4px; | ||
box-shadow: 0 2px 10px 0 #dedede; | ||
} | ||
|
||
.sf-metadata-merge-tags-selector .sf-metadata-search-tags-container { | ||
padding: 10px 10px 0; | ||
} | ||
|
||
.sf-metadata-merge-tags-selector .sf-metadata-search-tags-container .sf-metadata-search-tags { | ||
font-size: 14px; | ||
max-height: 30px; | ||
} | ||
|
||
.sf-metadata-merge-tags-selector .sf-metadata-merge-tags-selector-container { | ||
max-height: 200px; | ||
min-height: 100px; | ||
overflow: auto; | ||
padding: 10px; | ||
} | ||
|
||
.sf-metadata-merge-tags-selector .sf-metadata-merge-tags-selector-container .none-search-result { | ||
font-size: 14px; | ||
opacity: 0.5; | ||
display: inline-block; | ||
} | ||
|
||
.sf-metadata-merge-tags-selector .sf-metadata-tags-editor-tag-container { | ||
align-items: center; | ||
border-radius: 2px; | ||
color: #212529; | ||
display: flex; | ||
font-size: 13px; | ||
height: 30px; | ||
width: 100%; | ||
} | ||
|
||
.sf-metadata-merge-tags-selector .sf-metadata-tags-editor-tag-container-highlight { | ||
background: #f5f5f5; | ||
cursor: pointer; | ||
} | ||
|
||
.sf-metadata-tag-color-and-name { | ||
display: flex; | ||
align-items: center; | ||
flex: 1; | ||
} | ||
|
||
.sf-metadata-tag-color-and-name .sf-metadata-tag-color { | ||
height: 12px; | ||
width: 12px; | ||
border-radius: 50%; | ||
flex-shrink: 0; | ||
} | ||
|
||
.sf-metadata-tag-color-and-name .sf-metadata-tag-name { | ||
flex: 1; | ||
margin-left: 8px; | ||
overflow: hidden; | ||
text-overflow: ellipsis; | ||
white-space: nowrap; | ||
} |
202 changes: 202 additions & 0 deletions
202
frontend/src/tag/components/merge-tags-selector/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import classnames from 'classnames'; | ||
import { SearchInput, ClickOutside, ModalPortal } from '@seafile/sf-metadata-ui-component'; | ||
import { KeyCodes } from '../../../constants'; | ||
import { gettext } from '../../../utils/constants'; | ||
import { getRowsByIds } from '../../../components/sf-table/utils/table'; | ||
import { getTagColor, getTagId, getTagName, getTagsByNameOrColor } from '../../utils/cell'; | ||
import { EDITOR_CONTAINER as Z_INDEX_EDITOR_CONTAINER } from '../../../components/sf-table/constants/z-index'; | ||
import { useTags } from '../../hooks'; | ||
|
||
import './index.css'; | ||
|
||
const getInitTags = (mergeTagsIds, tagsData) => { | ||
if (!Array.isArray(mergeTagsIds) || mergeTagsIds.length === 0 || !tagsData || !Array.isArray(tagsData.row_ids)) return []; | ||
const sortedTagsIds = tagsData.row_ids.filter((tagId) => mergeTagsIds.includes(tagId)); | ||
if (sortedTagsIds.length === 0) return []; | ||
return getRowsByIds(tagsData, sortedTagsIds); | ||
}; | ||
|
||
const MergeTagsSelector = ({ | ||
mergeTagsIds, | ||
position = { left: 0, top: 0 }, | ||
closeSelector, | ||
mergeTags, | ||
}) => { | ||
const { tagsData } = useTags(); | ||
const [searchValue, setSearchValue] = useState(''); | ||
const [highlightIndex, setHighlightIndex] = useState(-1); | ||
const [maxItemNum, setMaxItemNum] = useState(0); | ||
const itemHeight = 30; | ||
const allTagsRef = useRef(getInitTags(mergeTagsIds, tagsData)); | ||
const editorContainerRef = useRef(null); | ||
const editorRef = useRef(null); | ||
const selectItemRef = useRef(null); | ||
|
||
const displayTags = useMemo(() => getTagsByNameOrColor(allTagsRef.current, searchValue), [searchValue, allTagsRef]); | ||
|
||
const onChangeSearch = useCallback((newSearchValue) => { | ||
if (searchValue === newSearchValue) return; | ||
setSearchValue(newSearchValue); | ||
}, [searchValue]); | ||
|
||
const onSelectTag = useCallback((targetTagId) => { | ||
const mergedTagsIds = mergeTagsIds.filter((tagId) => tagId !== targetTagId); | ||
mergeTags(targetTagId, mergedTagsIds); | ||
closeSelector(); | ||
}, [mergeTagsIds, closeSelector, mergeTags]); | ||
|
||
const onMenuMouseEnter = useCallback((highlightIndex) => { | ||
setHighlightIndex(highlightIndex); | ||
}, []); | ||
|
||
const onMenuMouseLeave = useCallback((index) => { | ||
setHighlightIndex(-1); | ||
}, []); | ||
|
||
const getMaxItemNum = useCallback(() => { | ||
let selectContainerStyle = getComputedStyle(editorContainerRef.current, null); | ||
let selectItemStyle = getComputedStyle(selectItemRef.current, null); | ||
let maxSelectItemNum = Math.floor(parseInt(selectContainerStyle.maxHeight) / parseInt(selectItemStyle.height)); | ||
return maxSelectItemNum - 1; | ||
}, [editorContainerRef, selectItemRef]); | ||
|
||
const onEnter = useCallback((event) => { | ||
event.preventDefault(); | ||
let tag; | ||
if (displayTags.length === 1) { | ||
tag = displayTags[0]; | ||
} else if (highlightIndex > -1) { | ||
tag = displayTags[highlightIndex]; | ||
} | ||
if (tag) { | ||
const newTagId = getTagId(tag); | ||
onSelectTag(newTagId); | ||
return; | ||
} | ||
}, [displayTags, highlightIndex, onSelectTag]); | ||
|
||
const onUpArrow = useCallback((event) => { | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
if (highlightIndex === 0) return; | ||
setHighlightIndex(highlightIndex - 1); | ||
if (highlightIndex > displayTags.length - maxItemNum) { | ||
editorContainerRef.current.scrollTop -= itemHeight; | ||
} | ||
}, [editorContainerRef, highlightIndex, maxItemNum, displayTags, itemHeight]); | ||
|
||
const onDownArrow = useCallback((event) => { | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
if (highlightIndex === displayTags.length - 1) return; | ||
setHighlightIndex(highlightIndex + 1); | ||
if (highlightIndex >= maxItemNum) { | ||
editorContainerRef.current.scrollTop += itemHeight; | ||
} | ||
}, [editorContainerRef, highlightIndex, maxItemNum, displayTags, itemHeight]); | ||
|
||
const onHotKey = useCallback((event) => { | ||
if (event.keyCode === KeyCodes.Enter) { | ||
onEnter(event); | ||
} else if (event.keyCode === KeyCodes.UpArrow) { | ||
onUpArrow(event); | ||
} else if (event.keyCode === KeyCodes.DownArrow) { | ||
onDownArrow(event); | ||
} | ||
}, [onEnter, onUpArrow, onDownArrow]); | ||
|
||
const onKeyDown = useCallback((event) => { | ||
if ( | ||
event.keyCode === KeyCodes.ChineseInputMethod || | ||
event.keyCode === KeyCodes.Enter || | ||
event.keyCode === KeyCodes.LeftArrow || | ||
event.keyCode === KeyCodes.RightArrow | ||
) { | ||
event.stopPropagation(); | ||
} | ||
}, []); | ||
|
||
useEffect(() => { | ||
if (editorRef.current) { | ||
const { bottom } = editorRef.current.getBoundingClientRect(); | ||
if (bottom > window.innerHeight) { | ||
editorRef.current.style.top = 'unset'; | ||
editorRef.current.style.bottom = '10px'; | ||
} | ||
} | ||
if (editorContainerRef.current && selectItemRef.current) { | ||
setMaxItemNum(getMaxItemNum()); | ||
} | ||
document.addEventListener('keydown', onHotKey, true); | ||
return () => { | ||
document.removeEventListener('keydown', onHotKey, true); | ||
}; | ||
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [onHotKey]); | ||
|
||
useEffect(() => { | ||
const highlightIndex = displayTags.length === 0 ? -1 : 0; | ||
setHighlightIndex(highlightIndex); | ||
}, [displayTags]); | ||
|
||
const renderOptions = useCallback(() => { | ||
if (displayTags.length === 0) { | ||
const noOptionsTip = searchValue ? gettext('No tags available') : gettext('No tag'); | ||
return (<span className="none-search-result">{noOptionsTip}</span>); | ||
} | ||
|
||
return displayTags.map((tag, i) => { | ||
const tagId = getTagId(tag); | ||
const tagName = getTagName(tag); | ||
const tagColor = getTagColor(tag); | ||
return ( | ||
<div key={tagId} className="sf-metadata-tags-editor-tag-item" ref={selectItemRef}> | ||
<div | ||
className={classnames('sf-metadata-tags-editor-tag-container pl-2', { 'sf-metadata-tags-editor-tag-container-highlight': i === highlightIndex })} | ||
onMouseDown={() => onSelectTag(tagId)} | ||
onMouseEnter={() => onMenuMouseEnter(i)} | ||
onMouseLeave={() => onMenuMouseLeave(i)} | ||
> | ||
<div className="sf-metadata-tag-color-and-name"> | ||
<div className="sf-metadata-tag-color" style={{ backgroundColor: tagColor }}></div> | ||
<div className="sf-metadata-tag-name">{tagName}</div> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}); | ||
|
||
}, [displayTags, searchValue, highlightIndex, onMenuMouseEnter, onMenuMouseLeave, onSelectTag]); | ||
|
||
return ( | ||
<ModalPortal> | ||
<ClickOutside onClickOutside={closeSelector}> | ||
<div className="sf-metadata-merge-tags-selector" style={{ ...position, position: 'fixed', width: 300, zIndex: Z_INDEX_EDITOR_CONTAINER }} ref={editorRef}> | ||
<div className="sf-metadata-search-tags-container"> | ||
<SearchInput | ||
autoFocus | ||
placeholder={gettext('Merge tags to:')} | ||
onKeyDown={onKeyDown} | ||
onChange={onChangeSearch} | ||
className="sf-metadata-search-tags" | ||
/> | ||
</div> | ||
<div className="sf-metadata-merge-tags-selector-container" ref={editorContainerRef}> | ||
{renderOptions()} | ||
</div> | ||
</div> | ||
</ClickOutside> | ||
</ModalPortal> | ||
); | ||
}; | ||
|
||
MergeTagsSelector.propTypes = { | ||
tagsTable: PropTypes.object, | ||
tags: PropTypes.array, | ||
position: PropTypes.object, | ||
}; | ||
|
||
export default MergeTagsSelector; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.