1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-04-28 03:10:45 +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:
Aries 2025-03-28 11:45:17 +08:00 committed by GitHub
parent 0abb343b4b
commit 005ddb4dca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 401 additions and 79 deletions

View File

@ -185,3 +185,16 @@ export const getTreeChildNodes = (parentNode, tree) => {
}
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;
};

View File

@ -16,11 +16,21 @@
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 {
font-size: 14px;
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 {
max-height: 200px;
min-height: 100px;
@ -28,20 +38,27 @@
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 {
font-size: 14px;
opacity: 0.5;
display: inline-block;
}
.sf-metadata-tags-editor .sf-metadata-tags-editor-tag-container {
align-items: center;
border-radius: 2px;
color: #212529;
.sf-metadata-tags-editor .sf-metadata-tags-editor-container .sf-metadata-tags-editor-title {
height: 32px;
display: flex;
font-size: 13px;
height: 30px;
width: 100%;
align-items: center;
padding: 0 16px;
color: #666;
}
.sf-metadata-tags-editor .sf-metadata-tags-editor-tag-container-highlight {
@ -54,23 +71,10 @@
width: 20px;
}
.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;
.sf-metadata-tags-editor .sf-metadata-tags-editor-divider {
border-top: 1px solid rgba(0, 40, 100, .12);
height: 0;
margin: 0.5rem 0;
opacity: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import classnames from 'classnames';
import CommonAddTool from '../../../../components/common-add-tool';
import SearchInput from '../../../../components/search-input';
import Icon from '../../../../components/icon';
import DeleteTags from './delete-tags';
import { Utils } from '../../../../utils/utils';
import { KeyCodes } from '../../../../constants';
@ -14,9 +13,13 @@ import { getRecordIdFromRecord } from '../../../utils/cell';
import { getRowById } from '../../../../components/sf-table/utils/table';
import { SELECT_OPTION_COLORS } from '../../../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';
const RECENTLY_USED_TAG_IDS = 'recently_used_tag_ids';
const TagsEditor = forwardRef(({
height,
column,
@ -25,6 +28,7 @@ const TagsEditor = forwardRef(({
editorPosition = { left: 0, top: 0 },
onPressTab,
updateFileTags,
showTagsAsTree,
}, ref) => {
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 [searchValue, setSearchValue] = useState('');
const [highlightIndex, setHighlightIndex] = useState(-1);
const [highlightNodeIndex, setHighlightNodeIndex] = useState(-1);
const [maxItemNum, setMaxItemNum] = useState(0);
const [nodes, setNodes] = useState([]);
const [keyNodeFoldedMap, setKeyNodeFoldedMap] = useState({});
const [recentlyUsed, setRecentlyUsed] = useState([]);
const itemHeight = 30;
const editorContainerRef = useRef(null);
const editorRef = useRef(null);
const selectItemRef = useRef(null);
const canEditData = window.sfMetadataContext.canModifyColumnData(column);
const localStorage = window.sfMetadataContext.localStorage;
const tags = useMemo(() => {
if (!tagsData) return [];
@ -46,6 +55,7 @@ const TagsEditor = forwardRef(({
}, [tagsData]);
const displayTags = useMemo(() => getTagsByNameOrColor(tags, searchValue), [searchValue, tags]);
const recentlyUsedTags = useMemo(() => recentlyUsed, [recentlyUsed]);
const isShowCreateBtn = useMemo(() => {
if (!canAddTag) return false;
@ -73,7 +83,16 @@ const TagsEditor = forwardRef(({
setValue(newValue);
const recordId = getRecordIdFromRecord(record);
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 newValue = value.slice(0);
@ -86,14 +105,22 @@ const TagsEditor = forwardRef(({
updateFileTags([{ record_id: recordId, tags: newValue, old_tags: value }]);
}, [value, record, updateFileTags]);
const onMenuMouseEnter = useCallback((highlightIndex) => {
setHighlightIndex(highlightIndex);
const onMenuMouseEnter = useCallback((i, id) => {
setHighlightIndex(i);
}, []);
const onMenuMouseLeave = useCallback((index) => {
const onMenuMouseLeave = useCallback(() => {
setHighlightIndex(-1);
}, []);
const onTreeMenuMouseEnter = useCallback((i) => {
setHighlightNodeIndex(i);
}, []);
const onTreeMenuMouseLeave = useCallback(() => {
setHighlightNodeIndex(-1);
}, []);
const createTag = useCallback((event) => {
event && event.stopPropagation();
event && event.nativeEvent.stopImmediatePropagation();
@ -115,18 +142,24 @@ const TagsEditor = forwardRef(({
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));
let maxSelectItemNum = Math.floor(parseInt(selectContainerStyle.maxHeight) / parseInt(itemHeight));
return maxSelectItemNum - 1;
}, [editorContainerRef, selectItemRef]);
}, [editorContainerRef]);
const onEnter = useCallback((event) => {
event.preventDefault();
let tag;
if (displayTags.length === 1) {
tag = displayTags[0];
} else if (highlightIndex > -1) {
tag = displayTags[highlightIndex];
if (showTagsAsTree) {
if (highlightNodeIndex > -1 && nodes[highlightNodeIndex]) {
const tagId = getTreeNodeId(nodes[highlightNodeIndex]);
tag = getRowById(tagsData, tagId);
}
} else {
if (displayTags.length === 1) {
tag = displayTags[0];
} else if (highlightIndex > -1) {
tag = displayTags[highlightIndex];
}
}
if (tag) {
const newTagId = getTagId(tag);
@ -136,27 +169,54 @@ const TagsEditor = forwardRef(({
if (isShowCreateBtn) {
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) => {
event.preventDefault();
event.stopPropagation();
if (highlightIndex === 0) return;
setHighlightIndex(highlightIndex - 1);
if (highlightIndex > displayTags.length - maxItemNum) {
editorContainerRef.current.scrollTop -= itemHeight;
if (showTagsAsTree) {
const newIndex = highlightNodeIndex - 1;
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) => {
event.preventDefault();
event.stopPropagation();
if (highlightIndex === displayTags.length - 1) return;
setHighlightIndex(highlightIndex + 1);
if (highlightIndex >= maxItemNum) {
editorContainerRef.current.scrollTop += itemHeight;
if (showTagsAsTree) {
const newIndex = highlightNodeIndex + 1;
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) => {
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(() => {
if (editorRef.current) {
const { bottom } = editorRef.current.getBoundingClientRect();
@ -191,7 +295,7 @@ const TagsEditor = forwardRef(({
editorRef.current.style.bottom = editorPosition.top + height - window.innerHeight + 'px';
}
}
if (editorContainerRef.current && selectItemRef.current) {
if (editorContainerRef.current) {
setMaxItemNum(getMaxItemNum());
}
document.addEventListener('keydown', onHotKey, true);
@ -202,9 +306,18 @@ const TagsEditor = forwardRef(({
}, [onHotKey]);
useEffect(() => {
const highlightIndex = displayTags.length === 0 ? -1 : 0;
setHighlightIndex(highlightIndex);
}, [displayTags]);
const saved = localStorage.getItem(RECENTLY_USED_TAG_IDS);
const ids = saved ? JSON.parse(saved) : [];
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(() => {
if (displayTags.length === 0) {
@ -214,33 +327,84 @@ const TagsEditor = forwardRef(({
return displayTags.map((tag, i) => {
const tagId = getTagId(tag);
const tagName = getTagName(tag);
const tagColor = getTagColor(tag);
const isSelected = Array.isArray(value) ? value.includes(tagId) : false;
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 className="sf-metadata-tags-editor-tag-check-icon">
{isSelected && (<Icon className="sf-metadata-icon" symbol="check-mark" />)}
</div>
</div>
</div>
<TagItem
key={tagId}
tag={tag}
isSelected={value.includes(tagId)}
highlight={highlightIndex === i}
onSelect={onSelectTag}
onMouseEnter={() => onMenuMouseEnter(i, tagId)}
onMouseLeave={onMenuMouseLeave}
/>
);
});
}, [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 (
<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="sf-metadata-tags-editor" style={style} ref={editorRef}>
<div className={classnames('sf-metadata-tags-editor', { 'tags-tree-container': showTagsAsTree })} style={style} ref={editorRef}>
<DeleteTags value={value} tags={tagsData} onDelete={onDeleteTag} />
<div className="sf-metadata-search-tags-container">
<SearchInput
@ -249,10 +413,21 @@ const TagsEditor = forwardRef(({
onChange={onChangeSearch}
autoFocus={true}
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 className="sf-metadata-tags-editor-container" ref={editorContainerRef}>
{renderOptions()}
{showTagsAsTree ? renderOptionsAsTree() : renderOptions()}
</div>
{isShowCreateBtn && (
<CommonAddTool
@ -272,6 +447,7 @@ TagsEditor.propTypes = {
editorPosition: PropTypes.object,
onPressTab: PropTypes.func,
updateFileTags: PropTypes.func,
showTagsAsTree: PropTypes.bool,
};
export default TagsEditor;

View File

@ -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;
}

View File

@ -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;

View File

@ -71,23 +71,36 @@ const TagsEditor = ({ record, value, field, updateFileTags }) => {
const renderEditor = useCallback(() => {
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 (
<Popover
target={ref}
isOpen={true}
placement="bottom-end"
placement={shouldPlaceBottom ? 'bottom-end' : 'top-end'}
hideArrow={true}
fade={false}
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
saveImmediately={true}
value={value}
column={{ ...field, width: Math.max(width - 2, 200) }}
column={{ ...field, width: Math.max(width - 2, 400) }}
record={record}
updateFileTags={updateFileTags}
showTagsAsTree={true}
/>
</Popover>
);