mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-28 11:41:18 +00:00
optimize tag editor ui (#7647)
* optimize tag editor ui * optimize selection by up/down key * optimize popover position * fix searched tree nodes folding --------- Co-authored-by: zhouwenxuan <aries@Mac.local>
This commit is contained in:
parent
0abb343b4b
commit
005ddb4dca
@ -185,3 +185,16 @@ export const getTreeChildNodes = (parentNode, tree) => {
|
|||||||
}
|
}
|
||||||
return childNodes;
|
return childNodes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getNodesWithAncestors = (node, tree) => {
|
||||||
|
const nodeKey = getTreeNodeKey(node);
|
||||||
|
|
||||||
|
let nodesWithAncestors = [];
|
||||||
|
tree.forEach((node, i) => {
|
||||||
|
if (!nodeKey.includes(getTreeNodeKey(node))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nodesWithAncestors.push({ ...node, node_index: i });
|
||||||
|
});
|
||||||
|
return nodesWithAncestors;
|
||||||
|
};
|
||||||
|
@ -16,11 +16,21 @@
|
|||||||
padding: 10px 10px 0;
|
padding: 10px 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-editor.tags-tree-container .sf-metadata-search-tags-container {
|
||||||
|
padding: 16px 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.sf-metadata-tags-editor .sf-metadata-search-tags-container .sf-metadata-search-tags {
|
.sf-metadata-tags-editor .sf-metadata-search-tags-container .sf-metadata-search-tags {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
max-height: 30px;
|
max-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-editor .sf-metadata-search-tags-container .search-control {
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
right: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-container {
|
.sf-metadata-tags-editor .sf-metadata-tags-editor-container {
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
@ -28,20 +38,27 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-editor.tags-tree-container .sf-metadata-tags-editor-container {
|
||||||
|
max-height: 280px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-editor.tags-tree-container .sf-metadata-tags-editor-container .sf-metadata-tags-editor-tag-item {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-container .none-search-result {
|
.sf-metadata-tags-editor .sf-metadata-tags-editor-container .none-search-result {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-tag-container {
|
.sf-metadata-tags-editor .sf-metadata-tags-editor-container .sf-metadata-tags-editor-title {
|
||||||
align-items: center;
|
height: 32px;
|
||||||
border-radius: 2px;
|
|
||||||
color: #212529;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 13px;
|
align-items: center;
|
||||||
height: 30px;
|
padding: 0 16px;
|
||||||
width: 100%;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-tag-container-highlight {
|
.sf-metadata-tags-editor .sf-metadata-tags-editor-tag-container-highlight {
|
||||||
@ -54,23 +71,10 @@
|
|||||||
width: 20px;
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-tag-color-and-name {
|
.sf-metadata-tags-editor .sf-metadata-tags-editor-divider {
|
||||||
display: flex;
|
border-top: 1px solid rgba(0, 40, 100, .12);
|
||||||
align-items: center;
|
height: 0;
|
||||||
flex: 1;
|
margin: 0.5rem 0;
|
||||||
}
|
opacity: 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;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import CommonAddTool from '../../../../components/common-add-tool';
|
import CommonAddTool from '../../../../components/common-add-tool';
|
||||||
import SearchInput from '../../../../components/search-input';
|
import SearchInput from '../../../../components/search-input';
|
||||||
import Icon from '../../../../components/icon';
|
|
||||||
import DeleteTags from './delete-tags';
|
import DeleteTags from './delete-tags';
|
||||||
import { Utils } from '../../../../utils/utils';
|
import { Utils } from '../../../../utils/utils';
|
||||||
import { KeyCodes } from '../../../../constants';
|
import { KeyCodes } from '../../../../constants';
|
||||||
@ -14,9 +13,13 @@ import { getRecordIdFromRecord } from '../../../utils/cell';
|
|||||||
import { getRowById } from '../../../../components/sf-table/utils/table';
|
import { getRowById } from '../../../../components/sf-table/utils/table';
|
||||||
import { SELECT_OPTION_COLORS } from '../../../constants';
|
import { SELECT_OPTION_COLORS } from '../../../constants';
|
||||||
import { PRIVATE_COLUMN_KEY as TAG_PRIVATE_COLUMN_KEY } from '../../../../tag/constants';
|
import { PRIVATE_COLUMN_KEY as TAG_PRIVATE_COLUMN_KEY } from '../../../../tag/constants';
|
||||||
|
import { checkIsTreeNodeShown, checkTreeNodeHasChildNodes, getNodesWithAncestors, getTreeNodeDepth, getTreeNodeId, getTreeNodeKey } from '../../../../components/sf-table/utils/tree';
|
||||||
|
import TagItem from './tag-item';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
|
const RECENTLY_USED_TAG_IDS = 'recently_used_tag_ids';
|
||||||
|
|
||||||
const TagsEditor = forwardRef(({
|
const TagsEditor = forwardRef(({
|
||||||
height,
|
height,
|
||||||
column,
|
column,
|
||||||
@ -25,6 +28,7 @@ const TagsEditor = forwardRef(({
|
|||||||
editorPosition = { left: 0, top: 0 },
|
editorPosition = { left: 0, top: 0 },
|
||||||
onPressTab,
|
onPressTab,
|
||||||
updateFileTags,
|
updateFileTags,
|
||||||
|
showTagsAsTree,
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const { tagsData, addTag, context } = useTags();
|
const { tagsData, addTag, context } = useTags();
|
||||||
|
|
||||||
@ -33,12 +37,17 @@ const TagsEditor = forwardRef(({
|
|||||||
const [value, setValue] = useState((oldValue || []).map(item => item.row_id).filter(item => getRowById(tagsData, item)));
|
const [value, setValue] = useState((oldValue || []).map(item => item.row_id).filter(item => getRowById(tagsData, item)));
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const [highlightIndex, setHighlightIndex] = useState(-1);
|
const [highlightIndex, setHighlightIndex] = useState(-1);
|
||||||
|
const [highlightNodeIndex, setHighlightNodeIndex] = useState(-1);
|
||||||
const [maxItemNum, setMaxItemNum] = useState(0);
|
const [maxItemNum, setMaxItemNum] = useState(0);
|
||||||
|
const [nodes, setNodes] = useState([]);
|
||||||
|
const [keyNodeFoldedMap, setKeyNodeFoldedMap] = useState({});
|
||||||
|
const [recentlyUsed, setRecentlyUsed] = useState([]);
|
||||||
const itemHeight = 30;
|
const itemHeight = 30;
|
||||||
const editorContainerRef = useRef(null);
|
const editorContainerRef = useRef(null);
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
const selectItemRef = useRef(null);
|
|
||||||
const canEditData = window.sfMetadataContext.canModifyColumnData(column);
|
const canEditData = window.sfMetadataContext.canModifyColumnData(column);
|
||||||
|
const localStorage = window.sfMetadataContext.localStorage;
|
||||||
|
|
||||||
const tags = useMemo(() => {
|
const tags = useMemo(() => {
|
||||||
if (!tagsData) return [];
|
if (!tagsData) return [];
|
||||||
@ -46,6 +55,7 @@ const TagsEditor = forwardRef(({
|
|||||||
}, [tagsData]);
|
}, [tagsData]);
|
||||||
|
|
||||||
const displayTags = useMemo(() => getTagsByNameOrColor(tags, searchValue), [searchValue, tags]);
|
const displayTags = useMemo(() => getTagsByNameOrColor(tags, searchValue), [searchValue, tags]);
|
||||||
|
const recentlyUsedTags = useMemo(() => recentlyUsed, [recentlyUsed]);
|
||||||
|
|
||||||
const isShowCreateBtn = useMemo(() => {
|
const isShowCreateBtn = useMemo(() => {
|
||||||
if (!canAddTag) return false;
|
if (!canAddTag) return false;
|
||||||
@ -73,7 +83,16 @@ const TagsEditor = forwardRef(({
|
|||||||
setValue(newValue);
|
setValue(newValue);
|
||||||
const recordId = getRecordIdFromRecord(record);
|
const recordId = getRecordIdFromRecord(record);
|
||||||
updateFileTags([{ record_id: recordId, tags: newValue, old_tags: value }]);
|
updateFileTags([{ record_id: recordId, tags: newValue, old_tags: value }]);
|
||||||
}, [value, record, updateFileTags]);
|
|
||||||
|
const ids = recentlyUsed.map(item => getTagId(item));
|
||||||
|
if (ids.indexOf(tagId) > -1) return;
|
||||||
|
const tag = getRowById(tagsData, tagId);
|
||||||
|
const updated = [tag, ...recentlyUsed.filter(item => getTagId(item) !== tagId)].slice(0, 2);
|
||||||
|
setRecentlyUsed(updated);
|
||||||
|
|
||||||
|
const newIds = updated.map(tag => getTagId(tag));
|
||||||
|
localStorage.setItem(RECENTLY_USED_TAG_IDS, JSON.stringify(newIds));
|
||||||
|
}, [value, record, tagsData, updateFileTags, recentlyUsed, localStorage]);
|
||||||
|
|
||||||
const onDeleteTag = useCallback((tagId) => {
|
const onDeleteTag = useCallback((tagId) => {
|
||||||
const newValue = value.slice(0);
|
const newValue = value.slice(0);
|
||||||
@ -86,14 +105,22 @@ const TagsEditor = forwardRef(({
|
|||||||
updateFileTags([{ record_id: recordId, tags: newValue, old_tags: value }]);
|
updateFileTags([{ record_id: recordId, tags: newValue, old_tags: value }]);
|
||||||
}, [value, record, updateFileTags]);
|
}, [value, record, updateFileTags]);
|
||||||
|
|
||||||
const onMenuMouseEnter = useCallback((highlightIndex) => {
|
const onMenuMouseEnter = useCallback((i, id) => {
|
||||||
setHighlightIndex(highlightIndex);
|
setHighlightIndex(i);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onMenuMouseLeave = useCallback((index) => {
|
const onMenuMouseLeave = useCallback(() => {
|
||||||
setHighlightIndex(-1);
|
setHighlightIndex(-1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onTreeMenuMouseEnter = useCallback((i) => {
|
||||||
|
setHighlightNodeIndex(i);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onTreeMenuMouseLeave = useCallback(() => {
|
||||||
|
setHighlightNodeIndex(-1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const createTag = useCallback((event) => {
|
const createTag = useCallback((event) => {
|
||||||
event && event.stopPropagation();
|
event && event.stopPropagation();
|
||||||
event && event.nativeEvent.stopImmediatePropagation();
|
event && event.nativeEvent.stopImmediatePropagation();
|
||||||
@ -115,19 +142,25 @@ const TagsEditor = forwardRef(({
|
|||||||
|
|
||||||
const getMaxItemNum = useCallback(() => {
|
const getMaxItemNum = useCallback(() => {
|
||||||
let selectContainerStyle = getComputedStyle(editorContainerRef.current, null);
|
let selectContainerStyle = getComputedStyle(editorContainerRef.current, null);
|
||||||
let selectItemStyle = getComputedStyle(selectItemRef.current, null);
|
let maxSelectItemNum = Math.floor(parseInt(selectContainerStyle.maxHeight) / parseInt(itemHeight));
|
||||||
let maxSelectItemNum = Math.floor(parseInt(selectContainerStyle.maxHeight) / parseInt(selectItemStyle.height));
|
|
||||||
return maxSelectItemNum - 1;
|
return maxSelectItemNum - 1;
|
||||||
}, [editorContainerRef, selectItemRef]);
|
}, [editorContainerRef]);
|
||||||
|
|
||||||
const onEnter = useCallback((event) => {
|
const onEnter = useCallback((event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let tag;
|
let tag;
|
||||||
|
if (showTagsAsTree) {
|
||||||
|
if (highlightNodeIndex > -1 && nodes[highlightNodeIndex]) {
|
||||||
|
const tagId = getTreeNodeId(nodes[highlightNodeIndex]);
|
||||||
|
tag = getRowById(tagsData, tagId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (displayTags.length === 1) {
|
if (displayTags.length === 1) {
|
||||||
tag = displayTags[0];
|
tag = displayTags[0];
|
||||||
} else if (highlightIndex > -1) {
|
} else if (highlightIndex > -1) {
|
||||||
tag = displayTags[highlightIndex];
|
tag = displayTags[highlightIndex];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (tag) {
|
if (tag) {
|
||||||
const newTagId = getTagId(tag);
|
const newTagId = getTagId(tag);
|
||||||
onSelectTag(newTagId);
|
onSelectTag(newTagId);
|
||||||
@ -136,27 +169,54 @@ const TagsEditor = forwardRef(({
|
|||||||
if (isShowCreateBtn) {
|
if (isShowCreateBtn) {
|
||||||
createTag();
|
createTag();
|
||||||
}
|
}
|
||||||
}, [displayTags, highlightIndex, isShowCreateBtn, onSelectTag, createTag]);
|
}, [displayTags, highlightIndex, isShowCreateBtn, onSelectTag, createTag, showTagsAsTree, tagsData, highlightNodeIndex, nodes]);
|
||||||
|
|
||||||
|
const updateScroll = useCallback((index) => {
|
||||||
|
const visibleStart = Math.floor(editorContainerRef.current.scrollTop / itemHeight);
|
||||||
|
const visibleEnd = visibleStart + maxItemNum;
|
||||||
|
|
||||||
|
if (index < visibleStart) {
|
||||||
|
editorContainerRef.current.scrollTop -= itemHeight;
|
||||||
|
} else if (index >= visibleEnd) {
|
||||||
|
editorContainerRef.current.scrollTop += itemHeight;
|
||||||
|
}
|
||||||
|
}, [maxItemNum]);
|
||||||
|
|
||||||
const onUpArrow = useCallback((event) => {
|
const onUpArrow = useCallback((event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (highlightIndex === 0) return;
|
|
||||||
setHighlightIndex(highlightIndex - 1);
|
if (showTagsAsTree) {
|
||||||
if (highlightIndex > displayTags.length - maxItemNum) {
|
const newIndex = highlightNodeIndex - 1;
|
||||||
editorContainerRef.current.scrollTop -= itemHeight;
|
if (newIndex < 0) return;
|
||||||
|
const pos = recentlyUsedTags.length > 0 ? newIndex + recentlyUsedTags.length + 2 : newIndex;
|
||||||
|
updateScroll(pos);
|
||||||
|
setHighlightNodeIndex(newIndex);
|
||||||
|
} else {
|
||||||
|
const newIndex = highlightIndex - 1;
|
||||||
|
if (newIndex < 0) return;
|
||||||
|
updateScroll(highlightIndex, displayTags.length, setHighlightIndex);
|
||||||
|
setHighlightIndex(newIndex);
|
||||||
}
|
}
|
||||||
}, [editorContainerRef, highlightIndex, maxItemNum, displayTags, itemHeight]);
|
}, [highlightIndex, displayTags, showTagsAsTree, recentlyUsedTags, highlightNodeIndex, updateScroll]);
|
||||||
|
|
||||||
const onDownArrow = useCallback((event) => {
|
const onDownArrow = useCallback((event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (highlightIndex === displayTags.length - 1) return;
|
|
||||||
setHighlightIndex(highlightIndex + 1);
|
if (showTagsAsTree) {
|
||||||
if (highlightIndex >= maxItemNum) {
|
const newIndex = highlightNodeIndex + 1;
|
||||||
editorContainerRef.current.scrollTop += itemHeight;
|
if (newIndex >= nodes.length) return;
|
||||||
|
const pos = recentlyUsedTags.length > 0 ? newIndex + recentlyUsedTags.length + 2 : newIndex;
|
||||||
|
updateScroll(pos);
|
||||||
|
setHighlightNodeIndex(newIndex);
|
||||||
|
} else {
|
||||||
|
const newIndex = highlightIndex + 1;
|
||||||
|
if (newIndex >= displayTags.length) return;
|
||||||
|
updateScroll(newIndex);
|
||||||
|
setHighlightIndex(newIndex);
|
||||||
}
|
}
|
||||||
}, [editorContainerRef, highlightIndex, maxItemNum, displayTags, itemHeight]);
|
}, [highlightIndex, displayTags, showTagsAsTree, nodes, recentlyUsedTags, highlightNodeIndex, updateScroll]);
|
||||||
|
|
||||||
const onHotKey = useCallback((event) => {
|
const onHotKey = useCallback((event) => {
|
||||||
if (event.keyCode === KeyCodes.Enter) {
|
if (event.keyCode === KeyCodes.Enter) {
|
||||||
@ -183,6 +243,50 @@ const TagsEditor = forwardRef(({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const getShownNodes = useCallback((tree, keyNodeFoldedMap) => {
|
||||||
|
if (!Array.isArray(tree)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let shownNodes = [];
|
||||||
|
|
||||||
|
tree.forEach((node, index) => {
|
||||||
|
const nodeId = getTreeNodeId(node);
|
||||||
|
const row = getRowById(tagsData, nodeId);
|
||||||
|
if (!row) return;
|
||||||
|
if (searchValue) {
|
||||||
|
const value = searchValue.toLowerCase();
|
||||||
|
const tagName = getTagName(row).toLowerCase();
|
||||||
|
const tagColor = getTagColor(row).toLowerCase();
|
||||||
|
if (!tagName.includes(value) && !tagColor.includes(value)) return;
|
||||||
|
if (showTagsAsTree) {
|
||||||
|
const nodesWithAncestors = getNodesWithAncestors(node, tree).filter(node => checkIsTreeNodeShown(getTreeNodeKey(node), keyNodeFoldedMap));
|
||||||
|
shownNodes = [...shownNodes, ...nodesWithAncestors];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nodeKey = getTreeNodeKey(node);
|
||||||
|
if (row && checkIsTreeNodeShown(nodeKey, keyNodeFoldedMap)) {
|
||||||
|
shownNodes.push({
|
||||||
|
...node,
|
||||||
|
node_index: index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return shownNodes;
|
||||||
|
}, [tagsData, searchValue, showTagsAsTree]);
|
||||||
|
|
||||||
|
const toggleExpandTreeNode = useCallback((nodeKey) => {
|
||||||
|
const updatedKeyNodeFoldedMap = { ...keyNodeFoldedMap };
|
||||||
|
if (updatedKeyNodeFoldedMap[nodeKey]) {
|
||||||
|
delete updatedKeyNodeFoldedMap[nodeKey];
|
||||||
|
} else {
|
||||||
|
updatedKeyNodeFoldedMap[nodeKey] = true;
|
||||||
|
}
|
||||||
|
const updatedNodes = getShownNodes(tagsData.rows_tree, updatedKeyNodeFoldedMap);
|
||||||
|
setNodes(updatedNodes);
|
||||||
|
setKeyNodeFoldedMap(updatedKeyNodeFoldedMap);
|
||||||
|
}, [tagsData, keyNodeFoldedMap, getShownNodes]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
const { bottom } = editorRef.current.getBoundingClientRect();
|
const { bottom } = editorRef.current.getBoundingClientRect();
|
||||||
@ -191,7 +295,7 @@ const TagsEditor = forwardRef(({
|
|||||||
editorRef.current.style.bottom = editorPosition.top + height - window.innerHeight + 'px';
|
editorRef.current.style.bottom = editorPosition.top + height - window.innerHeight + 'px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (editorContainerRef.current && selectItemRef.current) {
|
if (editorContainerRef.current) {
|
||||||
setMaxItemNum(getMaxItemNum());
|
setMaxItemNum(getMaxItemNum());
|
||||||
}
|
}
|
||||||
document.addEventListener('keydown', onHotKey, true);
|
document.addEventListener('keydown', onHotKey, true);
|
||||||
@ -202,9 +306,18 @@ const TagsEditor = forwardRef(({
|
|||||||
}, [onHotKey]);
|
}, [onHotKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const highlightIndex = displayTags.length === 0 ? -1 : 0;
|
const saved = localStorage.getItem(RECENTLY_USED_TAG_IDS);
|
||||||
setHighlightIndex(highlightIndex);
|
const ids = saved ? JSON.parse(saved) : [];
|
||||||
}, [displayTags]);
|
const tags = ids.map(id => getRowById(tagsData, id)).filter(Boolean);
|
||||||
|
setRecentlyUsed(tags);
|
||||||
|
}, [tagsData, localStorage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tagsData?.rows_tree) {
|
||||||
|
const shownNodes = getShownNodes(tagsData.rows_tree, keyNodeFoldedMap);
|
||||||
|
setNodes(shownNodes);
|
||||||
|
}
|
||||||
|
}, [tagsData, keyNodeFoldedMap, getShownNodes]);
|
||||||
|
|
||||||
const renderOptions = useCallback(() => {
|
const renderOptions = useCallback(() => {
|
||||||
if (displayTags.length === 0) {
|
if (displayTags.length === 0) {
|
||||||
@ -214,33 +327,84 @@ const TagsEditor = forwardRef(({
|
|||||||
|
|
||||||
return displayTags.map((tag, i) => {
|
return displayTags.map((tag, i) => {
|
||||||
const tagId = getTagId(tag);
|
const tagId = getTagId(tag);
|
||||||
const tagName = getTagName(tag);
|
|
||||||
const tagColor = getTagColor(tag);
|
|
||||||
const isSelected = Array.isArray(value) ? value.includes(tagId) : false;
|
|
||||||
return (
|
return (
|
||||||
<div key={tagId} className="sf-metadata-tags-editor-tag-item" ref={selectItemRef}>
|
<TagItem
|
||||||
<div
|
key={tagId}
|
||||||
className={classnames('sf-metadata-tags-editor-tag-container pl-2', { 'sf-metadata-tags-editor-tag-container-highlight': i === highlightIndex })}
|
tag={tag}
|
||||||
onMouseDown={() => onSelectTag(tagId)}
|
isSelected={value.includes(tagId)}
|
||||||
onMouseEnter={() => onMenuMouseEnter(i)}
|
highlight={highlightIndex === i}
|
||||||
onMouseLeave={() => onMenuMouseLeave(i)}
|
onSelect={onSelectTag}
|
||||||
>
|
onMouseEnter={() => onMenuMouseEnter(i, tagId)}
|
||||||
<div className="sf-metadata-tag-color-and-name">
|
onMouseLeave={onMenuMouseLeave}
|
||||||
<div className="sf-metadata-tag-color" style={{ backgroundColor: tagColor }}></div>
|
/>
|
||||||
<div className="sf-metadata-tag-name">{tagName}</div>
|
|
||||||
</div>
|
|
||||||
<div className="sf-metadata-tags-editor-tag-check-icon">
|
|
||||||
{isSelected && (<Icon className="sf-metadata-icon" symbol="check-mark" />)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
}, [displayTags, searchValue, value, highlightIndex, onMenuMouseEnter, onMenuMouseLeave, onSelectTag]);
|
}, [displayTags, searchValue, value, highlightIndex, onSelectTag, onMenuMouseEnter, onMenuMouseLeave]);
|
||||||
|
|
||||||
|
const renderRecentlyUsed = useCallback(() => {
|
||||||
|
return recentlyUsedTags.length > 0 && recentlyUsedTags.map((tag, i) => {
|
||||||
|
const tagId = getTagId(tag);
|
||||||
|
return (
|
||||||
|
<TagItem
|
||||||
|
key={tagId}
|
||||||
|
tag={tag}
|
||||||
|
isSelected={value.includes(tagId)}
|
||||||
|
highlight={highlightIndex === i}
|
||||||
|
onSelect={onSelectTag}
|
||||||
|
onMouseEnter={() => onMenuMouseEnter(i, tagId)}
|
||||||
|
onMouseLeave={onMenuMouseLeave}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
}, [recentlyUsedTags, value, highlightIndex, onSelectTag, onMenuMouseEnter, onMenuMouseLeave]);
|
||||||
|
|
||||||
|
const renderOptionsAsTree = useCallback(() => {
|
||||||
|
if (nodes.length === 0) {
|
||||||
|
const noOptionsTip = searchValue ? gettext('No tags available') : gettext('No tag');
|
||||||
|
return (<span className="none-search-result px-4">{noOptionsTip}</span>);
|
||||||
|
}
|
||||||
|
const showRecentlyUsed = recentlyUsedTags.length > 0 && !searchValue;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showRecentlyUsed && (
|
||||||
|
<>
|
||||||
|
<div className="sf-metadata-tags-editor-title">{gettext('Recently used tags')}</div>
|
||||||
|
{renderRecentlyUsed()}
|
||||||
|
<div className="sf-metadata-tags-editor-divider"></div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!searchValue && <div className="sf-metadata-tags-editor-title">{gettext('All tags')}</div>}
|
||||||
|
{nodes.map((node, i) => {
|
||||||
|
const nodeKey = getTreeNodeKey(node);
|
||||||
|
const tagId = getTreeNodeId(node);
|
||||||
|
const tag = getRowById(tagsData, tagId);
|
||||||
|
if (!tag) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sf-metadata-tags-editor" style={style} ref={editorRef}>
|
<TagItem
|
||||||
|
node={node}
|
||||||
|
key={`${nodeKey}_${i}`}
|
||||||
|
tag={tag}
|
||||||
|
isSelected={value.includes(tagId)}
|
||||||
|
highlight={highlightNodeIndex === i}
|
||||||
|
onSelect={onSelectTag}
|
||||||
|
onMouseEnter={() => onTreeMenuMouseEnter(i)}
|
||||||
|
onMouseLeave={onTreeMenuMouseLeave}
|
||||||
|
depth={getTreeNodeDepth(node)}
|
||||||
|
hasChildren={checkTreeNodeHasChildNodes(node)}
|
||||||
|
isFolded={keyNodeFoldedMap[nodeKey]}
|
||||||
|
onToggleExpand={() => toggleExpandTreeNode(nodeKey)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}, [nodes, tagsData, value, highlightNodeIndex, searchValue, recentlyUsedTags, renderRecentlyUsed, toggleExpandTreeNode, keyNodeFoldedMap, onSelectTag, onTreeMenuMouseEnter, onTreeMenuMouseLeave]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classnames('sf-metadata-tags-editor', { 'tags-tree-container': showTagsAsTree })} style={style} ref={editorRef}>
|
||||||
<DeleteTags value={value} tags={tagsData} onDelete={onDeleteTag} />
|
<DeleteTags value={value} tags={tagsData} onDelete={onDeleteTag} />
|
||||||
<div className="sf-metadata-search-tags-container">
|
<div className="sf-metadata-search-tags-container">
|
||||||
<SearchInput
|
<SearchInput
|
||||||
@ -249,10 +413,21 @@ const TagsEditor = forwardRef(({
|
|||||||
onChange={onChangeSearch}
|
onChange={onChangeSearch}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
className="sf-metadata-search-tags"
|
className="sf-metadata-search-tags"
|
||||||
|
isClearable={showTagsAsTree}
|
||||||
|
components={{
|
||||||
|
ClearIndicator: ({ clearValue }) => (
|
||||||
|
<i
|
||||||
|
className="search-control attr-action-icon sf3-font sf3-font-x-01"
|
||||||
|
aria-label={gettext('Clear')}
|
||||||
|
onClick={clearValue}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
clearValue={() => setSearchValue('')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="sf-metadata-tags-editor-container" ref={editorContainerRef}>
|
<div className="sf-metadata-tags-editor-container" ref={editorContainerRef}>
|
||||||
{renderOptions()}
|
{showTagsAsTree ? renderOptionsAsTree() : renderOptions()}
|
||||||
</div>
|
</div>
|
||||||
{isShowCreateBtn && (
|
{isShowCreateBtn && (
|
||||||
<CommonAddTool
|
<CommonAddTool
|
||||||
@ -272,6 +447,7 @@ TagsEditor.propTypes = {
|
|||||||
editorPosition: PropTypes.object,
|
editorPosition: PropTypes.object,
|
||||||
onPressTab: PropTypes.func,
|
onPressTab: PropTypes.func,
|
||||||
updateFileTags: PropTypes.func,
|
updateFileTags: PropTypes.func,
|
||||||
|
showTagsAsTree: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TagsEditor;
|
export default TagsEditor;
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
.sf-metadata-tags-editor .sf-metadata-tags-editor-tag-container {
|
||||||
|
position: relative;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: #212529;
|
||||||
|
display: flex;
|
||||||
|
font-size: 13px;
|
||||||
|
height: 30px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-editor .sf-metadata-tags-editor-tag-container .sf-metadata-tags-editor-tree-expand-icon {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: #666;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tag-color-and-name {
|
||||||
|
height: 100%;
|
||||||
|
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;
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { getTagColor, getTagId, getTagName } from '../../../../../tag/utils/cell';
|
||||||
|
import { NODE_CONTENT_LEFT_INDENT, NODE_ICON_LEFT_INDENT } from '../../../../../components/sf-table/constants/tree';
|
||||||
|
import Icon from '../../../../../components/icon';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const TagItem = ({
|
||||||
|
node,
|
||||||
|
tag,
|
||||||
|
isSelected,
|
||||||
|
highlight,
|
||||||
|
onSelect,
|
||||||
|
onMouseEnter,
|
||||||
|
onMouseLeave,
|
||||||
|
depth = 0,
|
||||||
|
hasChildren = false,
|
||||||
|
isFolded = false,
|
||||||
|
onToggleExpand
|
||||||
|
}) => {
|
||||||
|
const tagId = getTagId(tag);
|
||||||
|
const tagName = getTagName(tag);
|
||||||
|
const tagColor = getTagColor(tag);
|
||||||
|
const paddingLeft = node ? NODE_CONTENT_LEFT_INDENT + NODE_ICON_LEFT_INDENT * depth : 8;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sf-metadata-tags-editor-tag-item">
|
||||||
|
<div
|
||||||
|
className={classNames('sf-metadata-tags-editor-tag-container', {
|
||||||
|
'sf-metadata-tags-editor-tag-container-highlight': highlight,
|
||||||
|
})}
|
||||||
|
style={{ paddingLeft }}
|
||||||
|
onMouseDown={!node ? () => onSelect(tagId) : () => {}}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
>
|
||||||
|
{hasChildren && (
|
||||||
|
<span
|
||||||
|
className="sf-metadata-tags-editor-tree-expand-icon"
|
||||||
|
style={{ left: depth * NODE_ICON_LEFT_INDENT }}
|
||||||
|
onClick={onToggleExpand}
|
||||||
|
>
|
||||||
|
<i className={classNames('sf3-font sf3-font-down', { 'rotate-270': isFolded })}></i>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="sf-metadata-tag-color-and-name" onClick={node ? () => onSelect(tagId) : () => {}}>
|
||||||
|
<div className="sf-metadata-tag-color" style={{ backgroundColor: tagColor }} />
|
||||||
|
<div className="sf-metadata-tag-name">{tagName}</div>
|
||||||
|
</div>
|
||||||
|
<div className="sf-metadata-tags-editor-tag-check-icon">
|
||||||
|
{isSelected && <Icon className="sf-metadata-icon" symbol="check-mark" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TagItem.propTypes = {
|
||||||
|
node: PropTypes.object,
|
||||||
|
tag: PropTypes.object,
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
|
highlight: PropTypes.bool,
|
||||||
|
onSelect: PropTypes.func,
|
||||||
|
onMouseEnter: PropTypes.func,
|
||||||
|
onMouseLeave: PropTypes.func,
|
||||||
|
depth: PropTypes.number,
|
||||||
|
hasChildren: PropTypes.bool,
|
||||||
|
isFolded: PropTypes.bool,
|
||||||
|
onToggleExpand: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TagItem;
|
@ -71,23 +71,36 @@ const TagsEditor = ({ record, value, field, updateFileTags }) => {
|
|||||||
|
|
||||||
const renderEditor = useCallback(() => {
|
const renderEditor = useCallback(() => {
|
||||||
if (!showEditor) return null;
|
if (!showEditor) return null;
|
||||||
const { width } = ref.current.getBoundingClientRect();
|
const { width, bottom } = ref.current.getBoundingClientRect();
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const shouldPlaceBottom = (viewportHeight - bottom) > 300;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
target={ref}
|
target={ref}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
placement="bottom-end"
|
placement={shouldPlaceBottom ? 'bottom-end' : 'top-end'}
|
||||||
hideArrow={true}
|
hideArrow={true}
|
||||||
fade={false}
|
fade={false}
|
||||||
className="sf-metadata-property-editor-popover sf-metadata-tags-property-editor-popover"
|
className="sf-metadata-property-editor-popover sf-metadata-tags-property-editor-popover"
|
||||||
boundariesElement={document.body}
|
boundariesElement="viewport"
|
||||||
|
popperModifiers={{
|
||||||
|
preventOverflow: {
|
||||||
|
enabled: true,
|
||||||
|
padding: 8
|
||||||
|
},
|
||||||
|
offset: {
|
||||||
|
offset: '0, 8',
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Editor
|
<Editor
|
||||||
saveImmediately={true}
|
saveImmediately={true}
|
||||||
value={value}
|
value={value}
|
||||||
column={{ ...field, width: Math.max(width - 2, 200) }}
|
column={{ ...field, width: Math.max(width - 2, 400) }}
|
||||||
record={record}
|
record={record}
|
||||||
updateFileTags={updateFileTags}
|
updateFileTags={updateFileTags}
|
||||||
|
showTagsAsTree={true}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user