mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-02 15:38:15 +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:
1
frontend/src/assets/icons/tag.svg
Normal file
1
frontend/src/assets/icons/tag.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><!--Generated by IJSVG (https://github.com/iconjar/IJSVG)--><path d="M13.0123,19.3233l7.07107,-7.07107l-9.40841,-9.40842l-6.84754,-0.297719c-0.0144722,-0.00062923 -0.028965,-0.00062923 -0.0434372,0c-0.275882,0.0119949 -0.489804,0.245365 -0.477809,0.521247l0.297719,6.84754l9.40842,9.40842Zm-10.7052,-16.2125c-0.0359846,-0.827645 0.605783,-1.52776 1.43343,-1.56374c0.0434167,-0.00188768 0.086895,-0.00188768 0.130312,0l7.23616,0.314616l10.3906,10.3906l-8.48528,8.48528l-10.3906,-10.3906l-0.314616,-7.23616Zm5.75544,4.1917c-0.585786,0.585786 -1.53553,0.585786 -2.12132,0c-0.585786,-0.585786 -0.585786,-1.53553 0,-2.12132c0.585786,-0.585786 1.53553,-0.585786 2.12132,0c0.585786,0.585786 0.585786,1.53553 0,2.12132Zm-0.707107,-0.707107c0.195262,-0.195262 0.195262,-0.511845 0,-0.707107c-0.195262,-0.195262 -0.511845,-0.195262 -0.707107,0c-0.195262,0.195262 -0.195262,0.511845 0,0.707107c0.195262,0.195262 0.511845,0.195262 0.707107,0Z" fill="#979797" stroke="none"></path></svg>
|
After Width: | Height: | Size: 1.1 KiB |
22
frontend/src/components/common/common-add-tool.js
Normal file
22
frontend/src/components/common/common-add-tool.js
Normal 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;
|
101
frontend/src/components/common/seahub-popover.js
Normal file
101
frontend/src/components/common/seahub-popover.js
Normal 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;
|
144
frontend/src/components/common/search-input.js
Normal file
144
frontend/src/components/common/search-input.js
Normal 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;
|
@@ -20,7 +20,7 @@ const propTypes = {
|
|||||||
direntList: PropTypes.array,
|
direntList: PropTypes.array,
|
||||||
sortBy: PropTypes.string,
|
sortBy: PropTypes.string,
|
||||||
sortOrder: PropTypes.string,
|
sortOrder: PropTypes.string,
|
||||||
sortItems: PropTypes.array,
|
sortItems: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
class CurDirPath extends React.Component {
|
class CurDirPath extends React.Component {
|
||||||
|
@@ -2,6 +2,7 @@ import React, { Fragment } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, ModalHeader, ModalBody, ModalFooter, Input } from 'reactstrap';
|
import { Button, ModalHeader, ModalBody, ModalFooter, Input } from 'reactstrap';
|
||||||
import { gettext } from '../../utils/constants';
|
import { gettext } from '../../utils/constants';
|
||||||
|
import { TAG_COLORS } from '../../constants';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
|
|
||||||
@@ -17,10 +18,9 @@ class CreateTagDialog extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
tagName: '',
|
tagName: '',
|
||||||
tagColor: '',
|
tagColor: TAG_COLORS[0],
|
||||||
newTag: {},
|
newTag: {},
|
||||||
errorMsg: '',
|
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() {
|
render() {
|
||||||
let colorList = this.state.colorList;
|
|
||||||
let canSave = this.state.tagName.trim() ? true : false;
|
let canSave = this.state.tagName.trim() ? true : false;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -90,7 +83,7 @@ class CreateTagDialog extends React.Component {
|
|||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">{gettext('Select a color')}</label>
|
<label className="form-label">{gettext('Select a color')}</label>
|
||||||
<div className="d-flex justify-content-between">
|
<div className="d-flex justify-content-between">
|
||||||
{colorList.map((item, index)=>{
|
{TAG_COLORS.map((item, index)=>{
|
||||||
return (
|
return (
|
||||||
<div key={index} className="tag-color-option" onChange={this.selectTagcolor}>
|
<div key={index} className="tag-color-option" onChange={this.selectTagcolor}>
|
||||||
<label className="colorinput">
|
<label className="colorinput">
|
||||||
|
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { Popover, PopoverBody } from 'reactstrap';
|
import { Popover, PopoverBody } from 'reactstrap';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
|
import { TAG_COLORS } from '../../constants';
|
||||||
import toaster from '../toast';
|
import toaster from '../toast';
|
||||||
|
|
||||||
import '../../css/repo-tag.css';
|
import '../../css/repo-tag.css';
|
||||||
@@ -48,7 +49,7 @@ class TagColor extends React.Component {
|
|||||||
const { tag } = this.props;
|
const { tag } = this.props;
|
||||||
const { id, color } = tag;
|
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
|
// for color from previous color options
|
||||||
if (colorList.indexOf(color) == -1) {
|
if (colorList.indexOf(color) == -1) {
|
||||||
colorList.unshift(color);
|
colorList.unshift(color);
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import Icon from '../icon';
|
||||||
import { gettext } from '../../utils/constants';
|
import { gettext } from '../../utils/constants';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
import EditFileTagDialog from '../dialog/edit-filetag-dialog';
|
import EditFileTagDialog from '../dialog/edit-filetag-dialog';
|
||||||
import ModalPortal from '../modal-portal';
|
import ModalPortal from '../modal-portal';
|
||||||
import ExtraAttributesDialog from '../dialog/extra-attributes-dialog';
|
import ExtraAttributesDialog from '../dialog/extra-attributes-dialog';
|
||||||
|
import FileTagList from '../file-tag-list';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
repoInfo: PropTypes.object.isRequired,
|
repoInfo: PropTypes.object.isRequired,
|
||||||
@@ -64,7 +66,7 @@ class DetailListView extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderTags = () => {
|
renderTags = () => {
|
||||||
const { direntType, direntDetail, fileTagList } = this.props;
|
const { direntType, direntDetail } = this.props;
|
||||||
const position = this.getDirentPosition();
|
const position = this.getDirentPosition();
|
||||||
if (direntType === 'dir') {
|
if (direntType === 'dir') {
|
||||||
return (
|
return (
|
||||||
@@ -100,17 +102,8 @@ class DetailListView extends React.Component {
|
|||||||
<tr className="file-tag-container">
|
<tr className="file-tag-container">
|
||||||
<th>{gettext('Tags')}</th>
|
<th>{gettext('Tags')}</th>
|
||||||
<td>
|
<td>
|
||||||
<ul className="file-tag-list">
|
<FileTagList fileTagList={this.props.fileTagList} />
|
||||||
{Array.isArray(fileTagList) && fileTagList.map((fileTag) => {
|
<span onClick={this.onEditFileTagToggle}><Icon symbol='tag' /></span>
|
||||||
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>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{direntDetail.permission === 'rw' && (
|
{direntDetail.permission === 'rw' && (
|
||||||
|
@@ -6,6 +6,7 @@ import { seafileAPI } from '../../utils/seafile-api';
|
|||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
import toaster from '../toast';
|
import toaster from '../toast';
|
||||||
import FileTag from '../../models/file-tag';
|
import FileTag from '../../models/file-tag';
|
||||||
|
import FileTagList from '../file-tag-list';
|
||||||
|
|
||||||
import '../../css/dirent-detail.css';
|
import '../../css/dirent-detail.css';
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ class FileDetails extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderDetailBody = (bigIconUrl) => {
|
renderDetailBody = (bigIconUrl) => {
|
||||||
const { direntDetail, fileTagList } = this.state;
|
const { direntDetail } = this.state;
|
||||||
const { repoName, path } = this.props;
|
const { repoName, path } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="detail-body dirent-info">
|
<div className="detail-body dirent-info">
|
||||||
@@ -75,7 +76,10 @@ class FileDetails extends React.Component {
|
|||||||
<div className="dirent-table-container">
|
<div className="dirent-table-container">
|
||||||
<table className="table-thead-hidden">
|
<table className="table-thead-hidden">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th width="35%"></th><th width="65%"></th></tr>
|
<tr>
|
||||||
|
<th width="35%"></th>
|
||||||
|
<th width="65%"></th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><th>{gettext('Size')}</th><td>{Utils.bytesToSize(direntDetail.size)}</td></tr>
|
<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">
|
<tr className="file-tag-container">
|
||||||
<th>{gettext('Tags')}</th>
|
<th>{gettext('Tags')}</th>
|
||||||
<td>
|
<td>
|
||||||
<ul className="file-tag-list">
|
<FileTagList fileTagList={this.state.fileTagList} />
|
||||||
{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>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 moment from 'moment';
|
||||||
import { UncontrolledTooltip } from 'reactstrap';
|
import { Dropdown, DropdownToggle, DropdownItem, UncontrolledTooltip } from 'reactstrap';
|
||||||
import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
|
|
||||||
import { gettext, siteRoot, mediaUrl, username, useGoFileserver, fileServerRoot } from '../../utils/constants';
|
import { gettext, siteRoot, mediaUrl, username, useGoFileserver, fileServerRoot } from '../../utils/constants';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
@@ -16,6 +16,7 @@ import CopyDirentDialog from '../dialog/copy-dirent-dialog';
|
|||||||
import ShareDialog from '../dialog/share-dialog';
|
import ShareDialog from '../dialog/share-dialog';
|
||||||
import ZipDownloadDialog from '../dialog/zip-download-dialog';
|
import ZipDownloadDialog from '../dialog/zip-download-dialog';
|
||||||
import EditFileTagDialog from '../dialog/edit-filetag-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 LibSubFolderPermissionDialog from '../dialog/lib-sub-folder-permission-dialog';
|
||||||
import toaster from '../toast';
|
import toaster from '../toast';
|
||||||
import '../../css/dirent-list-item.css';
|
import '../../css/dirent-list-item.css';
|
||||||
@@ -88,6 +89,7 @@ class DirentListItem extends React.Component {
|
|||||||
isPermissionDialogOpen: false,
|
isPermissionDialogOpen: false,
|
||||||
isOpMenuOpen: false // for mobile
|
isOpMenuOpen: false // for mobile
|
||||||
};
|
};
|
||||||
|
this.tagListTitleID = `tag-list-title-${uuidv4()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
@@ -412,10 +414,8 @@ class DirentListItem extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onConvertWithONLYOFFICE = ()=> {
|
onConvertWithONLYOFFICE = ()=> {
|
||||||
|
|
||||||
let repoID = this.props.repoID;
|
let repoID = this.props.repoID;
|
||||||
let filePath = this.getDirentPath(this.props.dirent);
|
let filePath = this.getDirentPath(this.props.dirent);
|
||||||
|
|
||||||
seafileAPI.onlyofficeConvert(repoID, filePath).then(res => {
|
seafileAPI.onlyofficeConvert(repoID, filePath).then(res => {
|
||||||
this.props.loadDirentList(res.data.parent_dir);
|
this.props.loadDirentList(res.data.parent_dir);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
@@ -676,10 +676,8 @@ class DirentListItem extends React.Component {
|
|||||||
fileHref = siteRoot + 'lib/' + this.props.repoID + '/revisions/' + dirent.revision_id + '/';
|
fileHref = siteRoot + 'lib/' + this.props.repoID + '/revisions/' + dirent.revision_id + '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
let toolTipID = '';
|
|
||||||
let tagTitle = '';
|
let tagTitle = '';
|
||||||
if (dirent.file_tags && dirent.file_tags.length > 0) {
|
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(' ');
|
tagTitle = dirent.file_tags.map(item => item.name).join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,7 +689,6 @@ class DirentListItem extends React.Component {
|
|||||||
trClass += dirent.isSelected? 'tr-active' : '';
|
trClass += dirent.isSelected? 'tr-active' : '';
|
||||||
|
|
||||||
let lockedInfo = gettext('locked by {name}').replace('{name}', dirent.lock_owner_name);
|
let lockedInfo = gettext('locked by {name}').replace('{name}', dirent.lock_owner_name);
|
||||||
|
|
||||||
const isDesktop = Utils.isDesktop();
|
const isDesktop = Utils.isDesktop();
|
||||||
const { canDrag } = this.state;
|
const { canDrag } = this.state;
|
||||||
const desktopItem = (
|
const desktopItem = (
|
||||||
@@ -745,10 +742,10 @@ class DirentListItem extends React.Component {
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
</td>
|
</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) && (
|
{(dirent.type !== 'dir' && dirent.file_tags && dirent.file_tags.length > 0) && (
|
||||||
<Fragment>
|
<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) => {
|
{dirent.file_tags.map((fileTag, index) => {
|
||||||
let length = dirent.file_tags.length;
|
let length = dirent.file_tags.length;
|
||||||
return (
|
return (
|
||||||
@@ -756,7 +753,7 @@ class DirentListItem extends React.Component {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<UncontrolledTooltip target={`tag-list-title-${toolTipID}`} placement="bottom">
|
<UncontrolledTooltip target={this.tagListTitleID} placement="bottom">
|
||||||
{tagTitle}
|
{tagTitle}
|
||||||
</UncontrolledTooltip>
|
</UncontrolledTooltip>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@@ -851,6 +848,20 @@ class DirentListItem extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</ModalPortal>
|
</ModalPortal>
|
||||||
}
|
}
|
||||||
|
<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 &&
|
{this.state.isEditFileTagShow &&
|
||||||
<EditFileTagDialog
|
<EditFileTagDialog
|
||||||
repoID={this.props.repoID}
|
repoID={this.props.repoID}
|
||||||
@@ -860,6 +871,7 @@ class DirentListItem extends React.Component {
|
|||||||
onFileTagChanged={this.onFileTagChanged}
|
onFileTagChanged={this.onFileTagChanged}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
</MediaQuery>
|
||||||
{this.state.isZipDialogOpen &&
|
{this.state.isZipDialogOpen &&
|
||||||
<ModalPortal>
|
<ModalPortal>
|
||||||
<ZipDownloadDialog
|
<ZipDownloadDialog
|
||||||
|
25
frontend/src/components/file-tag-list.js
Normal file
25
frontend/src/components/file-tag-list.js
Normal 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;
|
@@ -12,6 +12,7 @@ const propTypes = {
|
|||||||
showDiffViewer: PropTypes.func.isRequired,
|
showDiffViewer: PropTypes.func.isRequired,
|
||||||
setDiffViewerContent: PropTypes.func.isRequired,
|
setDiffViewerContent: PropTypes.func.isRequired,
|
||||||
reloadDiffContent: PropTypes.func.isRequired,
|
reloadDiffContent: PropTypes.func.isRequired,
|
||||||
|
toggleHistoryPanel: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
class HistoryList extends React.Component {
|
class HistoryList extends React.Component {
|
||||||
@@ -143,7 +144,7 @@ const HistoryItempropTypes = {
|
|||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
index: PropTypes.number,
|
index: PropTypes.number,
|
||||||
preItem: PropTypes.object,
|
preItem: PropTypes.object,
|
||||||
currewntItem: PropTypes.object,
|
currentItem: PropTypes.object,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
208
frontend/src/components/popover/edit-filetag-popover.js
Normal file
208
frontend/src/components/popover/edit-filetag-popover.js
Normal 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;
|
87
frontend/src/components/popover/tag-item.js
Normal file
87
frontend/src/components/popover/tag-item.js
Normal 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;
|
@@ -106,7 +106,7 @@ export const DURATION_DECIMAL_DIGITS = {
|
|||||||
[DURATION_FORMATS_MAP.H_MM_SS_SSS]: 3,
|
[DURATION_FORMATS_MAP.H_MM_SS_SSS]: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
const TAG_COLORS = ['#FBD44A', '#EAA775', '#F4667C', '#DC82D2', '#9860E5', '#9F8CF1', '#59CB74', '#ADDF84',
|
||||||
KeyCodes,
|
'#89D2EA', '#4ECCCB', '#46A1FD', '#C2C2C2'];
|
||||||
zIndexes,
|
|
||||||
};
|
export { KeyCodes, zIndexes, TAG_COLORS };
|
||||||
|
33
frontend/src/css/common-add-tool.css
Normal file
33
frontend/src/css/common-add-tool.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
.add-item-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-top: 1px solid #dedede;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom-left-radius: 3px;
|
||||||
|
border-bottom-right-radius: 3px;
|
||||||
|
position: relative;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-btn:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-btn .dtable-icon-add-table {
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-btn .add-new-option {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
@@ -106,24 +106,10 @@
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-tag-list li {
|
.dirent-table-container .seafile-multicolor-icon.seafile-multicolor-icon-tag {
|
||||||
display: flex;
|
width: 20px;
|
||||||
align-items: center;
|
height: 20px;
|
||||||
max-width: 180px;
|
cursor: pointer;
|
||||||
}
|
|
||||||
|
|
||||||
.file-tag-list .file-tag-item .file-tag {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-tag-list .tag-name {
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 5px;
|
|
||||||
width: 80px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-related-files th {
|
.file-related-files th {
|
||||||
|
46
frontend/src/css/edit-filetag-popover.css
Normal file
46
frontend/src/css/edit-filetag-popover.css
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
.edit-filetag-popover .edit-filetag-popover-input {
|
||||||
|
max-height: 30px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #212529;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-filetag-popover .popover {
|
||||||
|
padding: 10px;
|
||||||
|
width: 200px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-filetag-popover .tag-list-container {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-filetag-popover .tag-list-container .tag-not-found {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-filetag-popover .tag-list-item {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-filetag-popover .tag-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 130px;
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
width: fit-content;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-filetag-popover .tag-item .tag-name {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-filetag-popover .add-item-btn {
|
||||||
|
margin: 0 -10px;
|
||||||
|
}
|
17
frontend/src/css/file-tag-list.css
Normal file
17
frontend/src/css/file-tag-list.css
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
.file-tag-list .file-tag-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 180px;
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
width: fit-content;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tag-list .file-tag-item .tag-name {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
@@ -35,19 +35,6 @@
|
|||||||
color: #444;
|
color: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-tag-item {
|
|
||||||
margin: 0.25rem 0;
|
|
||||||
padding: 0 0.5rem;
|
|
||||||
width: max-content;
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: #eee;
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-tag-item:hover {
|
|
||||||
background-color: #bbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-dialog-back {
|
.tag-dialog-back {
|
||||||
color: #888;
|
color: #888;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@@ -30,40 +30,6 @@
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-tag-list li {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-tag-list .file-tag-item {
|
|
||||||
margin: .25rem 0;
|
|
||||||
padding: 0 .5rem;
|
|
||||||
width: max-content;
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: #eee;
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-tag-list .file-tag-item .file-tag {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-tag-list .tag-name {
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 5px;
|
|
||||||
width: 80px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-related-files th {
|
.file-related-files th {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import Icon from '../../../components/icon';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import EditFileTagDialog from '../../../components/dialog/edit-filetag-dialog';
|
import EditFileTagDialog from '../../../components/dialog/edit-filetag-dialog';
|
||||||
|
import FileTagList from '../../../components/file-tag-list';
|
||||||
|
|
||||||
import '../../../css/dirent-detail.css';
|
import '../../../css/dirent-detail.css';
|
||||||
import '../css/detail-list-view.css';
|
import '../css/detail-list-view.css';
|
||||||
@@ -30,7 +32,7 @@ class DetailListView extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { fileTagList, fileInfo } = this.props;
|
const { fileInfo } = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="dirent-table-container p-2">
|
<div className="dirent-table-container p-2">
|
||||||
@@ -45,17 +47,8 @@ class DetailListView extends React.Component {
|
|||||||
<tr className="file-tag-container">
|
<tr className="file-tag-container">
|
||||||
<th>{gettext('Tags')}</th>
|
<th>{gettext('Tags')}</th>
|
||||||
<td>
|
<td>
|
||||||
<ul className="file-tag-list">
|
<FileTagList fileTagList={this.props.fileTagList} />
|
||||||
{Array.isArray(fileTagList) && fileTagList.map((fileTag) => {
|
<span onClick={this.onEditFileTagToggle}><Icon symbol='tag' /></span>
|
||||||
return (
|
|
||||||
<li key={fileTag.id} className="file-tag-item">
|
|
||||||
<span className="file-tag" style={{backgroundColor: fileTag.tag_color}}></span>
|
|
||||||
<span className="tag-name" title={fileTag.tag_name}>{fileTag.tag_name}</span>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
<i className='fa fa-pencil-alt attr-action-icon' onClick={this.onEditFileTagToggle}></i>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -65,7 +58,7 @@ class DetailListView extends React.Component {
|
|||||||
<EditFileTagDialog
|
<EditFileTagDialog
|
||||||
repoID={repoID}
|
repoID={repoID}
|
||||||
filePath={filePath}
|
filePath={filePath}
|
||||||
fileTagList={fileTagList}
|
fileTagList={this.props.fileTagList}
|
||||||
toggleCancel={this.onEditFileTagToggle}
|
toggleCancel={this.onEditFileTagToggle}
|
||||||
onFileTagChanged={this.props.onFileTagChanged}
|
onFileTagChanged={this.props.onFileTagChanged}
|
||||||
/>
|
/>
|
||||||
|
Reference in New Issue
Block a user