1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-25 14:50:29 +00:00

Optimize/tags filter UI (#7904)

* optimize tags editor

* update tags filter

* optimize

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
This commit is contained in:
Aries
2025-06-11 11:25:01 +08:00
committed by GitHub
parent 07de90eab0
commit 7aa257ce09
9 changed files with 119 additions and 115 deletions

View File

@@ -13,6 +13,7 @@
}
.sf-metadata-tags-editor .sf-metadata-search-tags-container {
position: relative;
padding: 10px 10px 0;
}

View File

@@ -13,6 +13,7 @@ import { SELECT_OPTION_COLORS } from '../../../constants';
import { PRIVATE_COLUMN_KEY as TAG_PRIVATE_COLUMN_KEY, RECENTLY_USED_TAG_IDS } from '../../../../tag/constants';
import { checkIsTreeNodeShown, checkTreeNodeHasChildNodes, getNodesWithAncestors, getTreeNodeDepth, getTreeNodeId, getTreeNodeKey } from '../../../../components/sf-table/utils/tree';
import TagItem from './tag-item';
import DeleteTag from './delete-tags';
import './index.css';
@@ -27,6 +28,8 @@ const TagsEditor = forwardRef(({
showTagsAsTree,
canEditData = false,
canAddTag = false,
showDeletableTags = false,
showRecentlyUsed = true,
}, ref) => {
const { tagsData, context, addTag } = useTags();
@@ -419,10 +422,10 @@ const TagsEditor = forwardRef(({
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;
const isRecentlyUsedVisible = showRecentlyUsed && recentlyUsedTags.length > 0 && !searchValue;
return (
<>
{showRecentlyUsed && (
{isRecentlyUsedVisible && (
<>
<div className="sf-metadata-tags-editor-title">{gettext('Recently used tags')}</div>
{renderRecentlyUsed()}
@@ -455,10 +458,11 @@ const TagsEditor = forwardRef(({
})}
</>
);
}, [nodes, tagsData, value, highlightNodeIndex, searchValue, recentlyUsedTags, renderRecentlyUsed, toggleExpandTreeNode, keyNodeFoldedMap, searchedKeyNodeFoldedMap, handleSelectTags, onTreeMenuMouseEnter, onTreeMenuMouseLeave]);
}, [nodes, tagsData, value, highlightNodeIndex, searchValue, recentlyUsedTags, keyNodeFoldedMap, searchedKeyNodeFoldedMap, showRecentlyUsed, renderRecentlyUsed, toggleExpandTreeNode, handleSelectTags, onTreeMenuMouseEnter, onTreeMenuMouseLeave]);
return (
<div className={classnames('sf-metadata-tags-editor', { 'tags-tree-container': showTagsAsTree })} style={style} ref={editorRef}>
{showDeletableTags && <DeleteTag value={value} tags={tagsData} onDelete={handleSelectTags} />}
<div className="sf-metadata-search-tags-container">
<SearchInput
placeholder={gettext('Search tag')}

View File

@@ -3,7 +3,6 @@ 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';
@@ -50,7 +49,7 @@ const TagItem = ({
<div className="sf-metadata-tag-name">{tagName}</div>
</div>
<div className="sf-metadata-tags-editor-tag-check-icon mr-1">
{isSelected && <Icon className="sf-metadata-icon" symbol="check-mark" />}
{isSelected && <i className="sf2-icon-tick"></i>}
</div>
</div>
</div>

View File

@@ -1,7 +1,6 @@
import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import CustomizeSelect from '../../../../../components/customize-select';
import Icon from '../../../../../components/icon';
import { gettext } from '../../../../../utils/constants';
const OPTIONS = [
@@ -21,7 +20,7 @@ const FileOrFolderFilter = ({ readOnly, value = 'all', onChange: onChangeAPI })
<div className="select-basic-filter-option">
<div className="select-basic-filter-option-name" title={name} aria-label={name}>{name}</div>
<div className="select-basic-filter-option-check-icon">
{value === o.value && (<Icon symbol="check-mark" />)}
{value === o.value && (<i className="sf2-icon-tick"></i>)}
</div>
</div>
)
@@ -31,13 +30,7 @@ const FileOrFolderFilter = ({ readOnly, value = 'all', onChange: onChangeAPI })
const displayValue = useMemo(() => {
const selectedOption = OPTIONS.find(o => o.value === value) || OPTIONS[2];
return {
label: (
<div>
{selectedOption.name}
</div>
)
};
return { label: <>{selectedOption.name}</> };
}, [value]);
const onChange = useCallback((newValue) => {

View File

@@ -1,7 +1,6 @@
import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import CustomizeSelect from '../../../../../components/customize-select';
import Icon from '../../../../../components/icon';
import { gettext } from '../../../../../utils/constants';
const OPTIONS = [
@@ -21,7 +20,7 @@ const GalleryFileTypeFilter = ({ readOnly, value = 'picture', onChange: onChange
<div className="select-basic-filter-option">
<div className="select-basic-filter-option-name" title={name} aria-label={name}>{name}</div>
<div className="select-basic-filter-option-check-icon">
{value === o.value && (<Icon symbol="check-mark" />)}
{value === o.value && (<i className="sf2-icon-tick"></i>)}
</div>
</div>
)
@@ -31,13 +30,7 @@ const GalleryFileTypeFilter = ({ readOnly, value = 'picture', onChange: onChange
const displayValue = useMemo(() => {
const selectedOption = OPTIONS.find(o => o.value === value) || OPTIONS[2];
return {
label: (
<div>
{selectedOption.name}
</div>
)
};
return { label: <>{selectedOption.name}</> };
}, [value]);
const onChange = useCallback((newValue) => {

View File

@@ -9,20 +9,42 @@
}
.sf-metadata-basic-filters-select.seafile-customize-select:hover {
background: #f5f5f5;
background: #efefef;
}
.sf-metadata-basic-filters-select.seafile-customize-select.highlighted:hover {
background-color: rgba(255, 152, 0, 0.2);
}
.sf-metadata-basic-filters-select.seafile-customize-select .selected-option {
background-color: inherit;
background-color: transparent;
}
.sf-metadata-basic-filters-select.seafile-customize-select .selected-option .sf3-font-down,
.sf-metadata-basic-filters-select.sf-metadata-basic-filter-tags-select .sf3-font-down {
margin-left: 0;
}
.sf-metadata-basic-filters-select.seafile-customize-select.focus {
border-color: unset;
box-shadow: none;
background-color: rgba(255, 152, 0, 0.2);
}
.sf-metadata-basic-filters-select .selected-option-show {
margin-right: 8px;
.sf-metadata-basic-filters-select.focus .selected-option .selected-option-show,
.sf-metadata-basic-filters-select.focus .selected-option .sf3-font-down,
.sf-metadata-basic-filters-select.highlighted .selected-option .selected-option-show,
.sf-metadata-basic-filters-select.highlighted .selected-option .sf3-font-down {
color: #ff9800;
}
.sf-metadata-basic-filters-select.seafile-customize-select .selected-option .selected-option-show {
width: 100%;
}
.sf-metadata-basic-filters-select .selected-option-show,
.sf-metadata-basic-filters-select .select-placeholder {
margin-right: 4px;
}
.sf-metadata-basic-filters-select .selected-option .custom-select-dropdown-icon {
@@ -75,3 +97,21 @@
.sf-metadata-table-view-basic-filter-file-type-select .select-basic-filter-option .select-basic-filter-option-checkbox input {
cursor: inherit;
}
.sf-metadata-basic-filters-select .select-basic-filter-display-name:hover {
cursor: pointer;
}
.sf-metadata-basic-filter-tags-select .tag-options-container {
width: auto;
height: auto;
position: absolute;
top: 28px;
left: -340px;
z-index: 10001;
}
.sf-metadata-basic-filter-tags-select .tag-options-container .sf-metadata-delete-select-tags {
padding: 8px 16px;
min-height: 28px;
}

View File

@@ -53,7 +53,7 @@ const BasicFilters = ({ readOnly, filters = [], onChange, viewType }) => {
return (<FileTypeFilter key={column_key} readOnly={readOnly} value={filter_term} onChange={onChangeFileTypeFilter} />);
}
if (column_key === PRIVATE_COLUMN_KEY.TAGS) {
return (<TagsFilter key={column_key} readOnly={readOnly} value={filter_term} onChange={onChangeTagsFilter} />);
return (<TagsFilter key={column_key} value={filter_term} onChange={onChangeTagsFilter} />);
}
return null;
})}

View File

@@ -1,5 +1,6 @@
import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import CustomizeSelect from '../../../../../components/customize-select';
import { gettext } from '../../../../../utils/constants';
import { getFileTypeColumnOptions } from '../../../../utils/column';
@@ -33,15 +34,8 @@ const TableFileTypeFilter = ({ readOnly, value, onChange: onChangeAPI }) => {
}, [OPTIONS, value]);
const displayValue = useMemo(() => {
const selectedOptions = OPTIONS.filter(o => value.includes(o.value));
return {
label: (
<div className="select-basic-filter-display-name">
{selectedOptions.length > 0 ? selectedOptions.map(o => o.name).join(', ') : gettext('File type')}
</div>
)
};
}, [OPTIONS, value]);
return { label: <>{gettext('File type')}</> };
}, []);
const onChange = useCallback((newValue) => {
if (value.includes(newValue)) {
@@ -54,7 +48,9 @@ const TableFileTypeFilter = ({ readOnly, value, onChange: onChangeAPI }) => {
return (
<CustomizeSelect
readOnly={readOnly}
className="sf-metadata-basic-filters-select sf-metadata-table-view-basic-filter-file-type-select mr-4"
className={classNames('sf-metadata-basic-filters-select sf-metadata-table-view-basic-filter-file-type-select mr-4', {
'highlighted': value.length > 0,
})}
value={displayValue}
options={options}
onSelectOption={onChange}

View File

@@ -1,21 +1,23 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { UncontrolledTooltip } from 'reactstrap';
import PropTypes from 'prop-types';
import CustomizeSelect from '../../../../../components/customize-select';
import Icon from '../../../../../components/icon';
import FileTagsFormatter from '../../../cell-formatter/file-tags';
import { gettext } from '../../../../../utils/constants';
import { useMetadataStatus } from '../../../../../hooks';
import { useTags } from '../../../../../tag/hooks';
import { getTagId, getTagName, getTagColor } from '../../../../../tag/utils/cell';
import { getRowById } from '../../../../../components/sf-table/utils/table';
import IconBtn from '../../../../../components/icon-btn';
import ClickOutside from '../../../../../components/click-outside';
import TagsEditor from '../../../cell-editors/tags-editor';
import classNames from 'classnames';
const TagsFilter = ({ readOnly, value: oldValue, onChange: onChangeAPI }) => {
const TagsFilter = ({ value: oldValue, onChange: onChangeAPI }) => {
const [isOptionsVisible, setIsOptionsVisible] = useState(false);
const { tagsData } = useTags();
const { enableTags } = useMetadataStatus();
const invalidFilterTip = React.createRef();
const invalidFilterTip = useRef(null);
const ref = useRef(null);
const value = useMemo(() => {
if (!enableTags) return [];
@@ -24,62 +26,22 @@ const TagsFilter = ({ readOnly, value: oldValue, onChange: onChangeAPI }) => {
return oldValue.filter(tagId => getRowById(tagsData, tagId));
}, [oldValue, tagsData, enableTags]);
const options = useMemo(() => {
if (!tagsData) return [];
const tags = tagsData?.rows || [];
if (tags.length === 0) return [];
return tags.map(tag => {
const tagId = getTagId(tag);
const tagName = getTagName(tag);
const tagColor = getTagColor(tag);
const isSelected = Array.isArray(value) ? value.includes(tagId) : false;
return {
name: tagName,
value: tagId,
label: (
<div className="select-basic-filter-option">
<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="select-basic-filter-option-check-icon">
{isSelected && (<Icon symbol="check-mark" />)}
</div>
</div>
)
};
});
}, [value, tagsData]);
const onSelectToggle = useCallback(() => {
setIsOptionsVisible(!isOptionsVisible);
}, [isOptionsVisible]);
const displayValue = useMemo(() => {
const emptyTip = {
label: (
<div className="select-basic-filter-display-name">
{gettext('Tags')}
</div>
),
};
if (!tagsData) return emptyTip;
const tags = tagsData?.rows || [];
if (tags.length === 0) emptyTip;
if (!Array.isArray(value) || value.length === 0) return emptyTip;
const selectedTags = value.map(tagId => getRowById(tagsData, tagId)).filter(item => item).map(tag => ({ row_id: getTagId(tag) }));
if (selectedTags.length === 0) return emptyTip;
return {
label: (
<div className="select-basic-filter-display-name">
<FileTagsFormatter className="sf-metadata-basic-tags-filter-formatter pr-2" tagsData={tagsData} value={selectedTags} />
</div>
)
};
}, [value, tagsData]);
const onChange = useCallback((newValue) => {
if (value.includes(newValue)) {
onChangeAPI(value.filter(v => v !== newValue));
} else {
onChangeAPI([...value, newValue]);
const onClickOutside = useCallback((e) => {
if (!ref.current.contains(e.target)) {
setIsOptionsVisible(false);
}
}, []);
const handleSelect = useCallback((tagId) => {
onChangeAPI([...value, tagId]);
}, [value, onChangeAPI]);
const handleDeselect = useCallback((tagId) => {
onChangeAPI(value.filter(v => v !== tagId));
}, [value, onChangeAPI]);
const onDeleteFilter = useCallback((event) => {
@@ -89,7 +51,6 @@ const TagsFilter = ({ readOnly, value: oldValue, onChange: onChangeAPI }) => {
oldValue = [];
}, [value, onChangeAPI, oldValue]);
const renderErrorMessage = () => {
return (
<div className="ml-2">
@@ -108,19 +69,38 @@ const TagsFilter = ({ readOnly, value: oldValue, onChange: onChangeAPI }) => {
);
};
const renderCustomizeSelect = () => {
const renderTagsTree = () => {
return (
<CustomizeSelect
readOnly={readOnly}
searchable={true}
supportMultipleSelect={true}
className="sf-metadata-basic-filters-select sf-metadata-table-view-basic-filter-file-type-select mr-4"
value={displayValue}
options={options}
onSelectOption={onChange}
searchPlaceholder={gettext('Search tag')}
noOptionsPlaceholder={gettext('No tags')}
/>
<div
ref={ref}
className={classNames('sf-metadata-basic-filters-select sf-metadata-basic-filter-tags-select seafile-customize-select custom-select mr-4', {
'focus': isOptionsVisible,
'highlighted': value.length > 0
})}
>
<div
className="selected-option"
onClick={onSelectToggle}
>
<span className="selected-option-show">{gettext('Tags')}</span>
<i className="sf3-font sf3-font-down" aria-hidden="true"></i>
</div>
{isOptionsVisible && (
<ClickOutside onClickOutside={onClickOutside}>
<div className="tag-options-container">
<TagsEditor
value={value.map(tagId => ({ row_id: tagId }))}
column={{ width: 400 }}
onSelect={handleSelect}
onDeselect={handleDeselect}
showTagsAsTree={true}
showDeletableTags={true}
showRecentlyUsed={false}
/>
</div>
</ClickOutside>
)}
</div>
);
};
@@ -128,7 +108,7 @@ const TagsFilter = ({ readOnly, value: oldValue, onChange: onChangeAPI }) => {
if (oldValue.length !== 0) {
return (
<div>
{renderCustomizeSelect()}
{renderTagsTree()}
{renderErrorMessage()}
<div className="delete-filter" onClick={onDeleteFilter}>
<Icon className="sf-metadata-icon" symbol="fork-number"/>
@@ -141,9 +121,7 @@ const TagsFilter = ({ readOnly, value: oldValue, onChange: onChangeAPI }) => {
}
return (
<div>
{renderCustomizeSelect()}
</div>
<>{renderTagsTree()}</>
);
};