1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-20 19:08:21 +00:00

Change tag list UI (#5637)

* 01 change file tags list

* 02 change select tags UI

* change edit icons
This commit is contained in:
Michael An
2023-09-13 23:12:23 +08:00
committed by GitHub
parent 14ce391007
commit 754d9c0fe8
22 changed files with 752 additions and 141 deletions

View File

@@ -0,0 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import '../../css/common-add-tool.css';
function CommonAddTool(props) {
const { callBack, footerName, className, addIconClassName } = props;
return (
<div className={`add-item-btn ${className ? className : ''}`} onClick={(e) => {callBack(e);}}>
<span className={`fas fa-plus mr-2 ${addIconClassName || ''}`}></span>
<span className='add-new-option' title={footerName}>{footerName}</span>
</div>
);
}
CommonAddTool.propTypes = {
className: PropTypes.string,
addIconClassName: PropTypes.string,
footerName: PropTypes.string.isRequired,
callBack: PropTypes.func.isRequired,
};
export default CommonAddTool;

View File

@@ -0,0 +1,101 @@
import React from 'react';
import { Popover } from 'reactstrap';
import PropTypes from 'prop-types';
import { KeyCodes } from '../../constants';
const propTypes = {
target: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
boundariesElement: PropTypes.object,
innerClassName: PropTypes.string,
popoverClassName: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
hideSeahubPopover: PropTypes.func.isRequired,
hideSeahubPopoverWithEsc: PropTypes.func,
hideArrow: PropTypes.bool,
canHideSeahubPopover: PropTypes.bool,
placement: PropTypes.string,
modifiers: PropTypes.object
};
class SeahubPopover extends React.Component {
SeahubPopoverRef = null;
isSelectOpen = false;
componentDidMount() {
document.addEventListener('mousedown', this.onMouseDown, true);
document.addEventListener('keydown', this.onKeyDown);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.onMouseDown, true);
document.removeEventListener('keydown', this.onKeyDown);
}
getEventClassName = (e) => {
// svg mouseEvent event.target.className is an object
if (!e || !e.target) return '';
return e.target.getAttribute('class') || '';
};
onKeyDown = (e) => {
const { canHideSeahubPopover, hideSeahubPopoverWithEsc } = this.props;
if (e.keyCode === KeyCodes.Escape && typeof hideSeahubPopoverWithEsc === 'function' && !this.isSelectOpen) {
e.preventDefault();
hideSeahubPopoverWithEsc();
} else if (e.keyCode === KeyCodes.Enter) {
// Resolve the default behavior of the enter key when entering formulas is blocked
if (canHideSeahubPopover) return;
e.stopImmediatePropagation();
}
};
onMouseDown = (e) => {
if (!this.props.canHideSeahubPopover) return;
if (this.SeahubPopoverRef && e && this.getEventClassName(e).indexOf('popover') === -1 && !this.SeahubPopoverRef.contains(e.target)) {
this.props.hideSeahubPopover(e);
}
};
onPopoverInsideClick = (e) => {
e.stopPropagation();
};
render() {
const {
target, boundariesElement, innerClassName, popoverClassName, hideArrow, modifiers,
placement,
} = this.props;
let additionalProps = {};
if (boundariesElement) {
additionalProps.boundariesElement = boundariesElement;
}
return (
<Popover
placement={placement}
isOpen={true}
target={target}
fade={false}
hideArrow={hideArrow}
innerClassName={innerClassName}
className={popoverClassName}
modifiers={modifiers}
{...additionalProps}
>
<div ref={ref => this.SeahubPopoverRef = ref} onClick={this.onPopoverInsideClick}>
{this.props.children}
</div>
</Popover>
);
}
}
SeahubPopover.defaultProps = {
placement: 'bottom-start',
hideArrow: true,
canHideSeahubPopover: true
};
SeahubPopover.propTypes = propTypes;
export default SeahubPopover;

View File

@@ -0,0 +1,144 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
const propTypes = {
placeholder: PropTypes.string,
autoFocus: PropTypes.bool,
className: PropTypes.string,
onChange: PropTypes.func.isRequired,
onKeyDown: PropTypes.func,
wait: PropTypes.number,
disabled: PropTypes.bool,
style: PropTypes.object,
isClearable: PropTypes.bool,
clearValue: PropTypes.func,
clearClassName: PropTypes.string,
components: PropTypes.object,
value: PropTypes.string,
};
class SearchInput extends Component {
constructor(props) {
super(props);
this.state = {
searchValue: props.value,
};
this.isInputtingChinese = false;
this.timer = null;
this.inputRef = null;
}
componentDidMount() {
if (this.props.autoFocus && this.inputRef && this.inputRef !== document.activeElement) {
setTimeout(() => {
this.inputRef.focus();
}, 0);
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.value !== this.props.value) {
this.setState({searchValue: nextProps.value});
}
}
componentWillUnmount() {
this.timer && clearTimeout(this.timer);
this.timer = null;
this.inputRef = null;
}
onCompositionStart = () => {
this.isInputtingChinese = true;
};
onChange = (e) => {
this.timer && clearTimeout(this.timer);
const { onChange, wait } = this.props;
let text = e.target.value;
this.setState({searchValue: text || ''}, () => {
if (this.isInputtingChinese) return;
this.timer = setTimeout(() => {
onChange && onChange(this.state.searchValue.trim());
}, wait);
});
};
onCompositionEnd = (e) => {
this.isInputtingChinese = false;
this.onChange(e);
};
clearSearch = () => {
const { clearValue } = this.props;
this.setState({searchValue: ''}, () => {
clearValue && clearValue();
});
};
setFocus = (isSelectAllText) => {
if (this.inputRef === document.activeElement) return;
this.inputRef.focus();
if (isSelectAllText) {
const txtLength = this.state.searchValue.length;
this.inputRef.setSelectionRange(0, txtLength);
}
};
isFunction = (functionToCheck) => {
const getType = {};
return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
};
renderClear = () => {
const { isClearable, clearClassName, components = {} } = this.props;
const { searchValue } = this.state;
if (!isClearable || !searchValue) return null;
const { ClearIndicator } = components;
if (React.isValidElement(ClearIndicator)) {
return React.cloneElement(ClearIndicator, {clearValue: this.clearSearch});
} else if (this.isFunction(ClearIndicator)) {
return <ClearIndicator clearValue={this.clearSearch} />;
}
return (
<i className={classnames('search-text-clear input-icon-addon', clearClassName)} onClick={this.clearSearch}>×</i>
);
};
render() {
const { placeholder, autoFocus, className, onKeyDown, disabled, style } = this.props;
const { searchValue } = this.state;
return (
<Fragment>
<input
type="text"
value={searchValue}
className={classnames('form-control', className)}
onChange={this.onChange}
autoFocus={autoFocus}
placeholder={placeholder}
onCompositionStart={this.onCompositionStart}
onCompositionEnd={this.onCompositionEnd}
onKeyDown={onKeyDown}
disabled={disabled}
style={style}
ref={ref => this.inputRef = ref}
/>
{this.renderClear()}
</Fragment>
);
}
}
SearchInput.propTypes = propTypes;
SearchInput.defaultProps = {
wait: 100,
disabled: false,
value: '',
};
export default SearchInput;

View File

@@ -20,7 +20,7 @@ const propTypes = {
direntList: PropTypes.array,
sortBy: PropTypes.string,
sortOrder: PropTypes.string,
sortItems: PropTypes.array,
sortItems: PropTypes.func,
};
class CurDirPath extends React.Component {

View File

@@ -2,6 +2,7 @@ import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Button, ModalHeader, ModalBody, ModalFooter, Input } from 'reactstrap';
import { gettext } from '../../utils/constants';
import { TAG_COLORS } from '../../constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
@@ -17,10 +18,9 @@ class CreateTagDialog extends React.Component {
super(props);
this.state = {
tagName: '',
tagColor: '',
tagColor: TAG_COLORS[0],
newTag: {},
errorMsg: '',
colorList: ['#FBD44A', '#EAA775', '#F4667C', '#DC82D2', '#9860E5', '#9F8CF1', '#59CB74', '#ADDF84', '#89D2EA', '#4ECCCB', '#46A1FD', '#C2C2C2'],
};
}
@@ -65,14 +65,7 @@ class CreateTagDialog extends React.Component {
}
};
componentDidMount() {
this.setState({
tagColor: this.state.colorList[0]
});
}
render() {
let colorList = this.state.colorList;
let canSave = this.state.tagName.trim() ? true : false;
return (
<Fragment>
@@ -90,7 +83,7 @@ class CreateTagDialog extends React.Component {
<div className="form-group">
<label className="form-label">{gettext('Select a color')}</label>
<div className="d-flex justify-content-between">
{colorList.map((item, index)=>{
{TAG_COLORS.map((item, index)=>{
return (
<div key={index} className="tag-color-option" onChange={this.selectTagcolor}>
<label className="colorinput">

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { Popover, PopoverBody } from 'reactstrap';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import { TAG_COLORS } from '../../constants';
import toaster from '../toast';
import '../../css/repo-tag.css';
@@ -48,7 +49,7 @@ class TagColor extends React.Component {
const { tag } = this.props;
const { id, color } = tag;
let colorList = ['#FBD44A', '#EAA775', '#F4667C', '#DC82D2', '#9860E5', '#9F8CF1', '#59CB74', '#ADDF84', '#89D2EA', '#4ECCCB', '#46A1FD', '#C2C2C2'];
let colorList = [...TAG_COLORS];
// for color from previous color options
if (colorList.indexOf(color) == -1) {
colorList.unshift(color);

View File

@@ -1,11 +1,13 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import Icon from '../icon';
import { gettext } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import EditFileTagDialog from '../dialog/edit-filetag-dialog';
import ModalPortal from '../modal-portal';
import ExtraAttributesDialog from '../dialog/extra-attributes-dialog';
import FileTagList from '../file-tag-list';
const propTypes = {
repoInfo: PropTypes.object.isRequired,
@@ -64,7 +66,7 @@ class DetailListView extends React.Component {
};
renderTags = () => {
const { direntType, direntDetail, fileTagList } = this.props;
const { direntType, direntDetail } = this.props;
const position = this.getDirentPosition();
if (direntType === 'dir') {
return (
@@ -100,17 +102,8 @@ class DetailListView extends React.Component {
<tr className="file-tag-container">
<th>{gettext('Tags')}</th>
<td>
<ul className="file-tag-list">
{Array.isArray(fileTagList) && fileTagList.map((fileTag) => {
return (
<li key={fileTag.id} className="file-tag-item">
<span className="file-tag" style={{backgroundColor:fileTag.color}}></span>
<span className="tag-name" title={fileTag.name}>{fileTag.name}</span>
</li>
);
})}
</ul>
<i className='fa fa-pencil-alt attr-action-icon' onClick={this.onEditFileTagToggle}></i>
<FileTagList fileTagList={this.props.fileTagList} />
<span onClick={this.onEditFileTagToggle}><Icon symbol='tag' /></span>
</td>
</tr>
{direntDetail.permission === 'rw' && (

View File

@@ -6,6 +6,7 @@ import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import toaster from '../toast';
import FileTag from '../../models/file-tag';
import FileTagList from '../file-tag-list';
import '../../css/dirent-detail.css';
@@ -66,7 +67,7 @@ class FileDetails extends React.Component {
};
renderDetailBody = (bigIconUrl) => {
const { direntDetail, fileTagList } = this.state;
const { direntDetail } = this.state;
const { repoName, path } = this.props;
return (
<div className="detail-body dirent-info">
@@ -75,7 +76,10 @@ class FileDetails extends React.Component {
<div className="dirent-table-container">
<table className="table-thead-hidden">
<thead>
<tr><th width="35%"></th><th width="65%"></th></tr>
<tr>
<th width="35%"></th>
<th width="65%"></th>
</tr>
</thead>
<tbody>
<tr><th>{gettext('Size')}</th><td>{Utils.bytesToSize(direntDetail.size)}</td></tr>
@@ -84,16 +88,7 @@ class FileDetails extends React.Component {
<tr className="file-tag-container">
<th>{gettext('Tags')}</th>
<td>
<ul className="file-tag-list">
{Array.isArray(fileTagList) && fileTagList.map((fileTag) => {
return (
<li key={fileTag.id} className="file-tag-item">
<span className="file-tag" style={{backgroundColor:fileTag.color}}></span>
<span className="tag-name" title={fileTag.name}>{fileTag.name}</span>
</li>
);
})}
</ul>
<FileTagList fileTagList={this.state.fileTagList} />
</td>
</tr>
</tbody>

View File

@@ -1,9 +1,9 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import MD5 from 'MD5';
import MediaQuery from 'react-responsive';
import { v4 as uuidv4 } from 'uuid';
import moment from 'moment';
import { UncontrolledTooltip } from 'reactstrap';
import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
import { Dropdown, DropdownToggle, DropdownItem, UncontrolledTooltip } from 'reactstrap';
import { gettext, siteRoot, mediaUrl, username, useGoFileserver, fileServerRoot } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import { seafileAPI } from '../../utils/seafile-api';
@@ -16,6 +16,7 @@ import CopyDirentDialog from '../dialog/copy-dirent-dialog';
import ShareDialog from '../dialog/share-dialog';
import ZipDownloadDialog from '../dialog/zip-download-dialog';
import EditFileTagDialog from '../dialog/edit-filetag-dialog';
import EditFileTagPopover from '../popover/edit-filetag-popover';
import LibSubFolderPermissionDialog from '../dialog/lib-sub-folder-permission-dialog';
import toaster from '../toast';
import '../../css/dirent-list-item.css';
@@ -88,6 +89,7 @@ class DirentListItem extends React.Component {
isPermissionDialogOpen: false,
isOpMenuOpen: false // for mobile
};
this.tagListTitleID = `tag-list-title-${uuidv4()}`;
}
componentWillReceiveProps(nextProps) {
@@ -412,10 +414,8 @@ class DirentListItem extends React.Component {
};
onConvertWithONLYOFFICE = ()=> {
let repoID = this.props.repoID;
let filePath = this.getDirentPath(this.props.dirent);
seafileAPI.onlyofficeConvert(repoID, filePath).then(res => {
this.props.loadDirentList(res.data.parent_dir);
}).catch(error => {
@@ -676,10 +676,8 @@ class DirentListItem extends React.Component {
fileHref = siteRoot + 'lib/' + this.props.repoID + '/revisions/' + dirent.revision_id + '/';
}
let toolTipID = '';
let tagTitle = '';
if (dirent.file_tags && dirent.file_tags.length > 0) {
toolTipID = MD5(dirent.name).slice(0, 7);
tagTitle = dirent.file_tags.map(item => item.name).join(' ');
}
@@ -691,7 +689,6 @@ class DirentListItem extends React.Component {
trClass += dirent.isSelected? 'tr-active' : '';
let lockedInfo = gettext('locked by {name}').replace('{name}', dirent.lock_owner_name);
const isDesktop = Utils.isDesktop();
const { canDrag } = this.state;
const desktopItem = (
@@ -745,10 +742,10 @@ class DirentListItem extends React.Component {
</Fragment>
)}
</td>
<td className="tag-list-title">
<td className="tag-list-title" id={this.tagListTitleID}>
{(dirent.type !== 'dir' && dirent.file_tags && dirent.file_tags.length > 0) && (
<Fragment>
<div id={`tag-list-title-${toolTipID}`} className="dirent-item tag-list tag-list-stacked">
<div className="dirent-item tag-list tag-list-stacked">
{dirent.file_tags.map((fileTag, index) => {
let length = dirent.file_tags.length;
return (
@@ -756,7 +753,7 @@ class DirentListItem extends React.Component {
);
})}
</div>
<UncontrolledTooltip target={`tag-list-title-${toolTipID}`} placement="bottom">
<UncontrolledTooltip target={this.tagListTitleID} placement="bottom">
{tagTitle}
</UncontrolledTooltip>
</Fragment>
@@ -851,15 +848,30 @@ class DirentListItem extends React.Component {
/>
</ModalPortal>
}
{this.state.isEditFileTagShow &&
<EditFileTagDialog
repoID={this.props.repoID}
fileTagList={dirent.file_tags}
filePath={direntPath}
toggleCancel={this.onEditFileTagToggle}
onFileTagChanged={this.onFileTagChanged}
/>
}
<MediaQuery query="(min-width: 768px)">
{this.state.isEditFileTagShow &&
<EditFileTagPopover
repoID={this.props.repoID}
fileTagList={dirent.file_tags}
filePath={direntPath}
toggleCancel={this.onEditFileTagToggle}
onFileTagChanged={this.onFileTagChanged}
target={this.tagListTitleID}
isEditFileTagShow={this.state.isEditFileTagShow}
/>
}
</MediaQuery>
<MediaQuery query="(max-width: 767.8px)">
{this.state.isEditFileTagShow &&
<EditFileTagDialog
repoID={this.props.repoID}
fileTagList={dirent.file_tags}
filePath={direntPath}
toggleCancel={this.onEditFileTagToggle}
onFileTagChanged={this.onFileTagChanged}
/>
}
</MediaQuery>
{this.state.isZipDialogOpen &&
<ModalPortal>
<ZipDownloadDialog

View File

@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import '../css/file-tag-list.css';
function FileTagList(props) {
return (
<ul className="file-tag-list">
{Array.isArray(props.fileTagList) && props.fileTagList.map((fileTag) => {
const color = fileTag.tag_color || fileTag.color;
const name = fileTag.tag_name || fileTag.name || '';
return (
<li key={fileTag.id} style={{backgroundColor: color}} className="file-tag-item">
<span className="tag-name" title={name}>{name}</span>
</li>
);
})}
</ul>
);
}
FileTagList.propTypes = {
fileTagList: PropTypes.array.isRequired,
};
export default FileTagList;

View File

@@ -12,6 +12,7 @@ const propTypes = {
showDiffViewer: PropTypes.func.isRequired,
setDiffViewerContent: PropTypes.func.isRequired,
reloadDiffContent: PropTypes.func.isRequired,
toggleHistoryPanel: PropTypes.func.isRequired,
};
class HistoryList extends React.Component {
@@ -143,7 +144,7 @@ const HistoryItempropTypes = {
onClick: PropTypes.func,
index: PropTypes.number,
preItem: PropTypes.object,
currewntItem: PropTypes.object,
currentItem: PropTypes.object,
name: PropTypes.string,
className: PropTypes.string,
};

View File

@@ -0,0 +1,208 @@
import React from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import RepoTag from '../../models/repo-tag';
import toaster from '../toast';
import CommonAddTool from '../common/common-add-tool';
import SearchInput from '../common/search-input';
import SeahubPopover from '../common/seahub-popover';
import TagItem from './tag-item';
import { KeyCodes, TAG_COLORS } from '../../constants';
import '../../css/repo-tag.css';
import '../../css/edit-filetag-popover.css';
class EditFileTagPopover extends React.Component {
constructor(props) {
super(props);
this.state = {
repotagList: [],
searchVal: '',
highlightIndex: -1,
};
}
componentDidMount() {
this.getRepoTagList();
}
setHighlightIndex = (highlightIndex) => {
this.setState({ highlightIndex });
};
getRepoTagList = () => {
let repoID = this.props.repoID;
seafileAPI.listRepoTags(repoID).then(res => {
let repotagList = [];
res.data.repo_tags.forEach(item => {
let repoTag = new RepoTag(item);
repotagList.push(repoTag);
});
this.setState({repotagList: repotagList});
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
generateRandomColor = () => {
return TAG_COLORS[Math.floor(Math.random() * TAG_COLORS.length)];
};
createNewTag = () => {
let name = this.state.searchVal.trim();
if (!name) return;
let color = this.generateRandomColor();
let repoID = this.props.repoID;
seafileAPI.createRepoTag(repoID, name, color).then((res) => {
let repoTagID = res.data.repo_tag.repo_tag_id;
this.onRepoTagCreated(repoTagID);
this.setState({
searchVal: '',
highlightIndex: -1,
});
this.getRepoTagList();
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
onRepoTagCreated = (repoTagID) => {
let {repoID, filePath} = this.props;
seafileAPI.addFileTag(repoID, filePath, repoTagID).then(() => {
this.props.onFileTagChanged();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
getRepoTagIdList = () => {
return (this.props.fileTagList || []).map((fileTag) => fileTag.repo_tag_id);
};
onEditFileTag = (repoTag) => {
let { repoID, filePath } = this.props;
let repoTagIdList = this.getRepoTagIdList();
if (repoTagIdList.indexOf(repoTag.id) === -1) {
seafileAPI.addFileTag(repoID, filePath, repoTag.id).then(() => {
repoTagIdList = this.getRepoTagIdList();
this.props.onFileTagChanged();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
} else {
let fileTag = null;
let fileTagList = this.props.fileTagList;
for(let i = 0; i < fileTagList.length; i++) {
if (fileTagList[i].repo_tag_id === repoTag.id) {
fileTag = fileTagList[i];
break;
}
}
seafileAPI.deleteFileTag(repoID, fileTag.id).then(() => {
repoTagIdList = this.getRepoTagIdList();
this.props.onFileTagChanged();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
};
onKeyDown = (e) => {
if (e.keyCode === KeyCodes.ChineseInputMethod || e.keyCode === KeyCodes.LeftArrow || e.keyCode === KeyCodes.RightArrow) {
e.stopPropagation();
}
else if (e.keyCode === KeyCodes.Enter) {
const searchText = this.state.searchVal.trim();
const repotagList = this.state.repotagList.filter(item => item.name.includes(searchText));
const tag = repotagList[this.state.highlightIndex];
if (tag) {
this.onEditFileTag(tag);
}
}
else if (e.keyCode === KeyCodes.UpArrow) {
if (this.state.highlightIndex > -1) {
this.setHighlightIndex(this.state.highlightIndex - 1);
}
}
else if (e.keyCode === KeyCodes.DownArrow) {
const searchText = this.state.searchVal.trim();
const repotagList = this.state.repotagList.filter(item => item.name.includes(searchText));
if (this.state.highlightIndex < repotagList.length) {
this.setHighlightIndex(this.state.highlightIndex + 1);
}
}
};
onChangeSearch = (searchVal) => {
this.setState({ searchVal });
this.setHighlightIndex(-1);
};
render() {
const searchText = this.state.searchVal.trim();
const repotagList = this.state.repotagList.filter(item => item.name.includes(searchText));
const showAddTool = searchText && !this.state.repotagList.find(item => item.name === searchText);
return (
<SeahubPopover
popoverClassName="edit-filetag-popover"
target={this.props.target}
hideSeahubPopover={this.props.toggleCancel}
hideSeahubPopoverWithEsc={this.props.toggleCancel}
canHideSeahubPopover={true}
>
<SearchInput
className="edit-filetag-popover-input"
placeholder={gettext('Find a tag')}
onKeyDown={this.onKeyDown}
onChange={this.onChangeSearch}
autoFocus={true}
/>
<ul className="tag-list-container">
{repotagList.length === 0 &&
<div className='tag-not-found mt-2 mb-4 mx-1'>{gettext('Tag not found')}</div>
}
{repotagList.length > 0 && repotagList.map((repoTag, index) => {
return (
<TagItem
index={index}
highlightIndex={this.state.highlightIndex}
setHighlightIndex={this.setHighlightIndex}
key={repoTag.id}
repoTag={repoTag}
repoID={this.props.repoID}
filePath={this.props.filePath}
fileTagList={this.props.fileTagList}
onFileTagChanged={this.props.onFileTagChanged}
/>
);
})}
</ul>
{showAddTool &&
<CommonAddTool
callBack={this.createNewTag}
footerName={`${gettext('Create a new tag')} '${searchText}'`}
/>
}
</SeahubPopover>
);
}
}
EditFileTagPopover.propTypes = {
target: PropTypes.string.isRequired,
repoID: PropTypes.string.isRequired,
filePath: PropTypes.string.isRequired,
fileTagList: PropTypes.array.isRequired,
toggleCancel: PropTypes.func.isRequired,
onFileTagChanged: PropTypes.func.isRequired,
};
export default EditFileTagPopover;

View File

@@ -0,0 +1,87 @@
import React from 'react';
import PropTypes from 'prop-types';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import toaster from '../toast';
class TagItem extends React.Component {
onMouseEnter = () => {
this.props.setHighlightIndex(this.props.index);
};
onMouseLeave = () => {
this.props.setHighlightIndex(-1);
};
getRepoTagIdList = () => {
let repoTagIdList = [];
let fileTagList = this.props.fileTagList || [];
repoTagIdList = fileTagList.map((fileTag) => fileTag.repo_tag_id);
return repoTagIdList;
};
onEditFileTag = () => {
let { repoID, repoTag, filePath } = this.props;
let repoTagIdList = this.getRepoTagIdList();
if (repoTagIdList.indexOf(repoTag.id) === -1) {
let id = repoTag.id;
seafileAPI.addFileTag(repoID, filePath, id).then(() => {
repoTagIdList = this.getRepoTagIdList();
this.props.onFileTagChanged();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
} else {
let fileTag = null;
let fileTagList = this.props.fileTagList;
for(let i = 0; i < fileTagList.length; i++) {
if (fileTagList[i].repo_tag_id === repoTag.id) {
fileTag = fileTagList[i];
break;
}
}
seafileAPI.deleteFileTag(repoID, fileTag.id).then(() => {
repoTagIdList = this.getRepoTagIdList();
this.props.onFileTagChanged();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
};
render() {
const { repoTag, highlightIndex, index } = this.props;
const repoTagIdList = this.getRepoTagIdList();
const isTagSelected = repoTagIdList.indexOf(repoTag.id) != -1;
return (
<li
className={`tag-list-item cursor-pointer px-3 d-flex justify-content-between align-items-center ${highlightIndex === index ? 'hl' : ''}`}
onClick={this.onEditFileTag}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
<div className="tag-item d-flex align-items-center" style={{backgroundColor: repoTag.color}}>
<span className="tag-name">{repoTag.name}</span>
</div>
{isTagSelected && <i className="fas fa-check tag-selected-icon"></i>}
</li>
);
}
}
TagItem.propTypes = {
index: PropTypes.number.isRequired,
highlightIndex: PropTypes.number.isRequired,
repoID: PropTypes.string.isRequired,
repoTag: PropTypes.object.isRequired,
filePath: PropTypes.string.isRequired,
fileTagList: PropTypes.array.isRequired,
onFileTagChanged: PropTypes.func.isRequired,
setHighlightIndex: PropTypes.func.isRequired,
};
export default TagItem;