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:
@@ -13,6 +13,7 @@
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor .sf-metadata-search-tags-container {
|
||||
position: relative;
|
||||
padding: 10px 10px 0;
|
||||
}
|
||||
|
||||
|
@@ -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')}
|
||||
|
@@ -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>
|
||||
|
@@ -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) => {
|
||||
|
@@ -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) => {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
})}
|
||||
|
@@ -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}
|
||||
|
@@ -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()}</>
|
||||
);
|
||||
|
||||
};
|
||||
|
Reference in New Issue
Block a user