mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-03 16:10:26 +00:00
Change edit tags dialog UI (#5655)
* fix warnings * 01 tags icon always show * 02 tag list footer UI * 03 change select color popover style * 04 Add virtual tag * 05 handle key event * 06 add createRepoTags API * 07 optimize codes * 08 optimize codes * optimize python code * change create tags success callback --------- Co-authored-by: wang <40563566+loveclever@users.noreply.github.com>
This commit is contained in:
@@ -1,12 +1,10 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { gettext, siteRoot } from '../../utils/constants';
|
import { gettext, siteRoot } from '../../utils/constants';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
import PropTypes from 'prop-types';
|
import SeahubPopover from '../common/seahub-popover';
|
||||||
import ModalPortal from '../modal-portal';
|
import ListTagPopover from '../popover/list-tag-popover';
|
||||||
import { Modal } from 'reactstrap';
|
|
||||||
import ListTagDialog from '../dialog/list-tag-dialog';
|
|
||||||
import CreateTagDialog from '../dialog/create-tag-dialog';
|
|
||||||
import ListTaggedFilesDialog from '../dialog/list-taggedfiles-dialog';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
repoID: PropTypes.string.isRequired,
|
repoID: PropTypes.string.isRequired,
|
||||||
@@ -21,112 +19,93 @@ class DirTool extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
isRepoTagDialogShow: false,
|
|
||||||
currentTag: null,
|
|
||||||
isListRepoTagShow: false,
|
isListRepoTagShow: false,
|
||||||
isCreateRepoTagShow: false,
|
|
||||||
isListTaggedFileShow: false,
|
|
||||||
};
|
};
|
||||||
|
this.tagsIconID = `tags-icon-${uuidv4()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
onShowListRepoTag = (e) => {
|
onShowListRepoTag = (e) => {
|
||||||
e.preventDefault();
|
this.setState({ isListRepoTagShow: true });
|
||||||
this.setState({
|
|
||||||
isRepoTagDialogShow: true,
|
|
||||||
isListRepoTagShow: true,
|
|
||||||
isCreateRepoTagShow: false,
|
|
||||||
isListTaggedFileShow: false
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onCloseRepoTagDialog = () => {
|
hidePopover = (e) => {
|
||||||
this.setState({
|
if (e) {
|
||||||
isRepoTagDialogShow: false,
|
let dom = e.target;
|
||||||
isListRepoTagShow: false,
|
while (dom) {
|
||||||
isCreateRepoTagShow: false,
|
if (typeof dom.className === 'string' && dom.className.includes('tag-color-popover')) return;
|
||||||
isListTaggedFileShow: false
|
dom = dom.parentNode;
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
this.setState({ isListRepoTagShow: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
onCreateRepoTagToggle = () => {
|
toggleCancel = () => {
|
||||||
this.setState({
|
this.setState({ isListRepoTagShow: false });
|
||||||
isCreateRepoTagShow: !this.state.isCreateRepoTagShow,
|
|
||||||
isListRepoTagShow: !this.state.isListRepoTagShow,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onListTaggedFileToggle = (currentTag) => {
|
|
||||||
this.setState({
|
|
||||||
currentTag: currentTag,
|
|
||||||
isListRepoTagShow: !this.state.isListRepoTagShow,
|
|
||||||
isListTaggedFileShow: !this.state.isListTaggedFileShow,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
isMarkdownFile(filePath) {
|
isMarkdownFile(filePath) {
|
||||||
let name = Utils.getFileName(filePath);
|
return Utils.getFileName(filePath).includes('.md');
|
||||||
return name.indexOf('.md') > -1 ? true : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let { repoID, userPerm, currentPath } = this.props;
|
let { repoID, userPerm, currentPath } = this.props;
|
||||||
let isFile = this.isMarkdownFile(currentPath);
|
if (userPerm !== 'rw') {
|
||||||
let name = Utils.getFileName(currentPath);
|
return '';
|
||||||
let trashUrl = siteRoot + 'repo/' + repoID + '/trash/';
|
}
|
||||||
let historyUrl = siteRoot + 'repo/history/' + repoID + '/';
|
if (this.isMarkdownFile(currentPath)) {
|
||||||
if (userPerm === 'rw') {
|
return '';
|
||||||
if (!isFile) {
|
}
|
||||||
if (name) { // name not '' is not root path
|
let toolbarDom = null;
|
||||||
trashUrl = siteRoot + 'repo/' + repoID + '/trash/?path=' + encodeURIComponent(currentPath);
|
if (Utils.getFileName(currentPath)) { // name not '' is not root path
|
||||||
return (
|
let trashUrl = siteRoot + 'repo/' + repoID + '/trash/?path=' + encodeURIComponent(currentPath);
|
||||||
|
toolbarDom = (
|
||||||
<ul className="path-toolbar">
|
<ul className="path-toolbar">
|
||||||
<li className="toolbar-item"><a className="op-link sf2-icon-recycle" href={trashUrl} title={gettext('Trash')} aria-label={gettext('Trash')}></a></li>
|
<li className="toolbar-item">
|
||||||
|
<a className="op-link sf2-icon-tag" href="#" id={this.tagsIconID} role="button" onClick={this.onShowListRepoTag} title={gettext('Tags')} aria-label={gettext('Tags')}></a>
|
||||||
|
</li>
|
||||||
|
<li className="toolbar-item">
|
||||||
|
<a className="op-link sf2-icon-recycle" href={trashUrl} title={gettext('Trash')} aria-label={gettext('Trash')}></a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
} else { // currentPath === '/' is root path
|
} else { // currentPath === '/' is root path
|
||||||
return (
|
let trashUrl = siteRoot + 'repo/' + repoID + '/trash/';
|
||||||
<Fragment>
|
let historyUrl = siteRoot + 'repo/history/' + repoID + '/';
|
||||||
|
toolbarDom = (
|
||||||
<ul className="path-toolbar">
|
<ul className="path-toolbar">
|
||||||
<li className="toolbar-item"><a className="op-link sf2-icon-tag" href="#" role="button" onClick={this.onShowListRepoTag} title={gettext('Tags')} aria-label={gettext('Tags')}></a></li>
|
<li className="toolbar-item">
|
||||||
<li className="toolbar-item"><a className="op-link sf2-icon-recycle" href={trashUrl} title={gettext('Trash')} aria-label={gettext('Trash')}></a></li>
|
<a className="op-link sf2-icon-tag" href="#" id={this.tagsIconID} role="button" onClick={this.onShowListRepoTag} title={gettext('Tags')} aria-label={gettext('Tags')}></a>
|
||||||
<li className="toolbar-item"><a className="op-link sf2-icon-history" href={historyUrl} title={gettext('History')} aria-label={gettext('History')}></a></li>
|
</li>
|
||||||
|
<li className="toolbar-item">
|
||||||
|
<a className="op-link sf2-icon-recycle" href={trashUrl} title={gettext('Trash')} aria-label={gettext('Trash')}></a>
|
||||||
|
</li>
|
||||||
|
<li className="toolbar-item">
|
||||||
|
<a className="op-link sf2-icon-history" href={historyUrl} title={gettext('History')} aria-label={gettext('History')}></a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{this.state.isRepoTagDialogShow && (
|
|
||||||
<ModalPortal>
|
|
||||||
<Modal isOpen={true} autoFocus={false}>
|
|
||||||
{this.state.isListRepoTagShow && (
|
|
||||||
<ListTagDialog
|
|
||||||
repoID={repoID}
|
|
||||||
onListTagCancel={this.onCloseRepoTagDialog}
|
|
||||||
onCreateRepoTag={this.onCreateRepoTagToggle}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{this.state.isCreateRepoTagShow && (
|
|
||||||
<CreateTagDialog
|
|
||||||
repoID={repoID}
|
|
||||||
onClose={this.onCloseRepoTagDialog}
|
|
||||||
toggleCancel={this.onCreateRepoTagToggle}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{this.state.isListTaggedFileShow && (
|
|
||||||
<ListTaggedFilesDialog
|
|
||||||
repoID={this.props.repoID}
|
|
||||||
currentTag={this.state.currentTag}
|
|
||||||
onClose={this.onCloseRepoTagDialog}
|
|
||||||
toggleCancel={this.onListTaggedFileToggle}
|
|
||||||
updateUsedRepoTags={this.props.updateUsedRepoTags}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
</ModalPortal>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{toolbarDom}
|
||||||
|
{this.state.isListRepoTagShow &&
|
||||||
|
<SeahubPopover
|
||||||
|
popoverClassName="list-tag-popover"
|
||||||
|
target={this.tagsIconID}
|
||||||
|
hideSeahubPopover={this.hidePopover}
|
||||||
|
hideSeahubPopoverWithEsc={this.hidePopover}
|
||||||
|
canHideSeahubPopover={true}
|
||||||
|
boundariesElement={document.body}
|
||||||
|
placement={'bottom-end'}
|
||||||
|
>
|
||||||
|
<ListTagPopover
|
||||||
|
repoID={repoID}
|
||||||
|
onListTagCancel={this.toggleCancel}
|
||||||
|
/>
|
||||||
|
</SeahubPopover>
|
||||||
}
|
}
|
||||||
}
|
</>
|
||||||
return '';
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,158 +0,0 @@
|
|||||||
import React, { Fragment } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Button, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
|
|
||||||
import { gettext } from '../../utils/constants';
|
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
|
||||||
import { Utils } from '../../utils/utils';
|
|
||||||
import toaster from '../toast';
|
|
||||||
import RepoTag from '../../models/repo-tag';
|
|
||||||
import TagColor from './tag-color';
|
|
||||||
import TagName from './tag-name';
|
|
||||||
|
|
||||||
import '../../css/repo-tag.css';
|
|
||||||
|
|
||||||
const tagListItemPropTypes = {
|
|
||||||
item: PropTypes.object.isRequired,
|
|
||||||
repoID: PropTypes.string.isRequired,
|
|
||||||
onDeleteTag : PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
class TagListItem extends React.Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
isTagHighlighted: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseOver = () => {
|
|
||||||
this.setState({
|
|
||||||
isTagHighlighted: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onMouseOut = () => {
|
|
||||||
this.setState({
|
|
||||||
isTagHighlighted: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteTag = () => {
|
|
||||||
this.props.onDeleteTag(this.props.item);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { isTagHighlighted } = this.state;
|
|
||||||
const { item, repoID } = this.props;
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
className={`tag-list-item px-4 d-flex justify-content-between align-items-center ${isTagHighlighted ? 'hl' : ''}`}
|
|
||||||
onMouseOver={this.onMouseOver}
|
|
||||||
onMouseOut={this.onMouseOut}
|
|
||||||
>
|
|
||||||
<TagColor repoID={repoID} tag={item} />
|
|
||||||
<TagName repoID={repoID} tag={item} />
|
|
||||||
<button
|
|
||||||
className={`tag-delete-icon sf2-icon-delete border-0 px-0 bg-transparent cursor-pointer ${isTagHighlighted ? '' : 'invisible'}`}
|
|
||||||
onClick={this.deleteTag}
|
|
||||||
aria-label={gettext('Delete')}
|
|
||||||
title={gettext('Delete')}
|
|
||||||
></button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TagListItem.propTypes = tagListItemPropTypes;
|
|
||||||
|
|
||||||
const listTagPropTypes = {
|
|
||||||
repoID: PropTypes.string.isRequired,
|
|
||||||
onListTagCancel: PropTypes.func.isRequired,
|
|
||||||
onCreateRepoTag: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
class ListTagDialog extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
repotagList: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
let repoID = this.props.repoID;
|
|
||||||
seafileAPI.listRepoTags(repoID).then(res => {
|
|
||||||
let repotagList = [];
|
|
||||||
res.data.repo_tags.forEach(item => {
|
|
||||||
let repo_tag = new RepoTag(item);
|
|
||||||
repotagList.push(repo_tag);
|
|
||||||
});
|
|
||||||
this.setState({
|
|
||||||
repotagList: repotagList
|
|
||||||
});
|
|
||||||
}).catch(error => {
|
|
||||||
let errMessage = Utils.getErrorMsg(error);
|
|
||||||
toaster.danger(errMessage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle = () => {
|
|
||||||
this.props.onListTagCancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
createNewTag = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onCreateRepoTag();
|
|
||||||
};
|
|
||||||
|
|
||||||
onDeleteTag = (tag) => {
|
|
||||||
const { repoID } = this.props;
|
|
||||||
const { id: targetTagID } = tag;
|
|
||||||
seafileAPI.deleteRepoTag(repoID, targetTagID).then((res) => {
|
|
||||||
this.setState({
|
|
||||||
repotagList: this.state.repotagList.filter(tag => tag.id != targetTagID)
|
|
||||||
});
|
|
||||||
}).catch((error) => {
|
|
||||||
let errMessage = Utils.getErrorMsg(error);
|
|
||||||
toaster.danger(errMessage);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<ModalHeader toggle={this.toggle}>{gettext('Tags')}</ModalHeader>
|
|
||||||
<ModalBody className="px-0">
|
|
||||||
<ul className="tag-list tag-list-container">
|
|
||||||
{this.state.repotagList.map((repoTag, index) => {
|
|
||||||
return (
|
|
||||||
<TagListItem
|
|
||||||
key={index}
|
|
||||||
item={repoTag}
|
|
||||||
repoID={this.props.repoID}
|
|
||||||
onDeleteTag={this.onDeleteTag}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="add-tag-link px-4 py-2 d-flex align-items-center"
|
|
||||||
onClick={this.createNewTag}
|
|
||||||
>
|
|
||||||
<span className="sf2-icon-plus mr-2"></span>
|
|
||||||
{gettext('Create a new tag')}
|
|
||||||
</a>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button color="secondary" onClick={this.toggle}>{gettext('Close')}</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ListTagDialog.propTypes = listTagPropTypes;
|
|
||||||
|
|
||||||
export default ListTagDialog;
|
|
@@ -23,6 +23,14 @@ class TagColor extends React.Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
|
if (nextProps.tag.color !== this.props.tag.color) {
|
||||||
|
this.setState({
|
||||||
|
tagColor: nextProps.tag.color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
togglePopover = () => {
|
togglePopover = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isPopoverOpen: !this.state.isPopoverOpen
|
isPopoverOpen: !this.state.isPopoverOpen
|
||||||
@@ -59,7 +67,7 @@ class TagColor extends React.Component {
|
|||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
id={`tag-${id}-color`}
|
id={`tag-${id}-color`}
|
||||||
className="tag-color cursor-pointer w-4 h-4 rounded-circle d-flex align-items-center justify-content-center"
|
className="tag-color cursor-pointer rounded-circle d-flex align-items-center justify-content-center"
|
||||||
style={{backgroundColor: tagColor}}
|
style={{backgroundColor: tagColor}}
|
||||||
onClick={this.togglePopover}
|
onClick={this.togglePopover}
|
||||||
>
|
>
|
||||||
@@ -70,7 +78,7 @@ class TagColor extends React.Component {
|
|||||||
isOpen={isPopoverOpen}
|
isOpen={isPopoverOpen}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
toggle={this.togglePopover}
|
toggle={this.togglePopover}
|
||||||
className="mw-100"
|
className="tag-color-popover mw-100"
|
||||||
>
|
>
|
||||||
<PopoverBody className="p-2">
|
<PopoverBody className="p-2">
|
||||||
<div className="d-flex justify-content-between">
|
<div className="d-flex justify-content-between">
|
||||||
|
@@ -22,6 +22,14 @@ class TagName extends React.Component {
|
|||||||
this.input = React.createRef();
|
this.input = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
|
if (nextProps.tag.name !== this.props.tag.name) {
|
||||||
|
this.setState({
|
||||||
|
tagName: nextProps.tag.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
toggleMode = () => {
|
toggleMode = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isEditing: !this.state.isEditing
|
isEditing: !this.state.isEditing
|
||||||
@@ -51,6 +59,10 @@ class TagName extends React.Component {
|
|||||||
this.toggleMode();
|
this.toggleMode();
|
||||||
this.updateTagName(e);
|
this.updateTagName(e);
|
||||||
}
|
}
|
||||||
|
else if (e.key == 'Escape') {
|
||||||
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
|
this.toggleMode();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onInputBlur = (e) => {
|
onInputBlur = (e) => {
|
||||||
|
@@ -48,8 +48,8 @@ const propTypes = {
|
|||||||
onAddFolder: PropTypes.func.isRequired,
|
onAddFolder: PropTypes.func.isRequired,
|
||||||
showDirentDetail: PropTypes.func.isRequired,
|
showDirentDetail: PropTypes.func.isRequired,
|
||||||
onItemRename: PropTypes.func.isRequired,
|
onItemRename: PropTypes.func.isRequired,
|
||||||
posX: PropTypes.number.isRequired,
|
posX: PropTypes.number,
|
||||||
posY: PropTypes.number.isRequired,
|
posY: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
class DirentGridView extends React.Component {
|
class DirentGridView extends React.Component {
|
||||||
|
@@ -54,7 +54,7 @@ const propTypes = {
|
|||||||
showDirentDetail: PropTypes.func.isRequired,
|
showDirentDetail: PropTypes.func.isRequired,
|
||||||
onItemsMove: PropTypes.func.isRequired,
|
onItemsMove: PropTypes.func.isRequired,
|
||||||
onShowDirentsDraggablePreview: PropTypes.func,
|
onShowDirentsDraggablePreview: PropTypes.func,
|
||||||
loadDirentList: PropTypes.func.isRequired,
|
loadDirentList: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
class DirentListItem extends React.Component {
|
class DirentListItem extends React.Component {
|
||||||
|
31
frontend/src/components/popover/list-tag-popover.css
Normal file
31
frontend/src/components/popover/list-tag-popover.css
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
.list-tag-popover .popover {
|
||||||
|
width: 500px;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-tag-popover .add-tag-link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-tag-popover .tag-list-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 1rem;
|
||||||
|
border-top: 1px solid #dedede;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-tag-popover .tag-list-footer .item-text {
|
||||||
|
color: #ff8000;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-tag-popover .tag-list-footer a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-tag-popover .tag-color {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
154
frontend/src/components/popover/list-tag-popover.js
Normal file
154
frontend/src/components/popover/list-tag-popover.js
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { gettext } from '../../utils/constants';
|
||||||
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
|
import { Utils } from '../../utils/utils';
|
||||||
|
import toaster from '../toast';
|
||||||
|
import RepoTag from '../../models/repo-tag';
|
||||||
|
import TagListItem from './tag-list-item';
|
||||||
|
import VirtualTagListItem from './virtual-tag-list-item';
|
||||||
|
import TagListFooter from './tag-list-footer';
|
||||||
|
import { TAG_COLORS } from '../../constants/';
|
||||||
|
|
||||||
|
import '../../css/repo-tag.css';
|
||||||
|
import './list-tag-popover.css';
|
||||||
|
|
||||||
|
export default class ListTagPopover extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
repoID: PropTypes.string.isRequired,
|
||||||
|
onListTagCancel: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
repotagList: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.loadTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTags = () => {
|
||||||
|
seafileAPI.listRepoTags(this.props.repoID).then(res => {
|
||||||
|
let repotagList = [];
|
||||||
|
res.data.repo_tags.forEach(item => {
|
||||||
|
let repo_tag = new RepoTag(item);
|
||||||
|
repotagList.push(repo_tag);
|
||||||
|
});
|
||||||
|
this.setState({ repotagList });
|
||||||
|
}).catch(error => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
updateTags = (repotagList) => {
|
||||||
|
this.setState({ repotagList });
|
||||||
|
};
|
||||||
|
|
||||||
|
onDeleteTag = (tag) => {
|
||||||
|
const { repoID } = this.props;
|
||||||
|
const { id: targetTagID } = tag;
|
||||||
|
seafileAPI.deleteRepoTag(repoID, targetTagID).then((res) => {
|
||||||
|
this.setState({
|
||||||
|
repotagList: this.state.repotagList.filter(tag => tag.id != targetTagID)
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
createVirtualTag = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
let { repotagList } = this.state;
|
||||||
|
let virtual_repo_tag = {
|
||||||
|
name: '',
|
||||||
|
color: TAG_COLORS[Math.floor(Math.random() * TAG_COLORS.length)], // generate random tag color for virtual tag
|
||||||
|
id: `virtual-tag-${uuidv4()}`,
|
||||||
|
is_virtual: true,
|
||||||
|
};
|
||||||
|
repotagList.push(virtual_repo_tag);
|
||||||
|
this.setState({ repotagList });
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteVirtualTag = (virtualTag) => {
|
||||||
|
let { repotagList } = this.state;
|
||||||
|
let index = repotagList.findIndex(item => item.id === virtualTag.id);
|
||||||
|
repotagList.splice(index, 1);
|
||||||
|
this.setState({ repotagList });
|
||||||
|
};
|
||||||
|
|
||||||
|
updateVirtualTag = (virtualTag, data) => {
|
||||||
|
const repoID = this.props.repoID;
|
||||||
|
const { repotagList } = this.state;
|
||||||
|
const index = repotagList.findIndex(item => item.id === virtualTag.id);
|
||||||
|
if (index < 0) return null;
|
||||||
|
|
||||||
|
// If virtual tag color is updated and virtual tag name is empty, it will be saved to local state, don't save it to the server
|
||||||
|
if (data.color) {
|
||||||
|
virtualTag.color = data.color;
|
||||||
|
repotagList[index] = virtualTag;
|
||||||
|
this.setState({ repotagList });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If virtual tag name is updated and name is not empty, virtual tag color use default, save it to the server
|
||||||
|
if (data.name && data.name.length > 0) {
|
||||||
|
let color = virtualTag.color;
|
||||||
|
let name = data.name;
|
||||||
|
seafileAPI.createRepoTag(repoID, name, color).then((res) => {
|
||||||
|
// After saving sag to the server, replace the virtual tag with newly created tag
|
||||||
|
repotagList[index] = new RepoTag(res.data.repo_tag);
|
||||||
|
this.setState({ repotagList });
|
||||||
|
}).catch((error) => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<ul className="tag-list tag-list-container my-2">
|
||||||
|
{this.state.repotagList.map((repoTag, index) => {
|
||||||
|
if (repoTag.is_virtual) {
|
||||||
|
return (
|
||||||
|
<VirtualTagListItem
|
||||||
|
key={index}
|
||||||
|
item={repoTag}
|
||||||
|
repoID={this.props.repoID}
|
||||||
|
deleteVirtualTag={this.deleteVirtualTag}
|
||||||
|
updateVirtualTag={this.updateVirtualTag}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<TagListItem
|
||||||
|
key={index}
|
||||||
|
item={repoTag}
|
||||||
|
repoID={this.props.repoID}
|
||||||
|
onDeleteTag={this.onDeleteTag}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<div className="add-tag-link px-4 py-2 d-flex align-items-center" onClick={this.createVirtualTag}>
|
||||||
|
<span className="sf2-icon-plus mr-2"></span>{gettext('Create a new tag')}
|
||||||
|
</div>
|
||||||
|
<TagListFooter
|
||||||
|
toggle={this.props.onListTagCancel}
|
||||||
|
repotagList={this.state.repotagList}
|
||||||
|
updateTags={this.updateTags}
|
||||||
|
repoID={this.props.repoID}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
137
frontend/src/components/popover/tag-list-footer.js
Normal file
137
frontend/src/components/popover/tag-list-footer.js
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Tooltip } from 'reactstrap';
|
||||||
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
|
import { gettext } from '../../utils/constants';
|
||||||
|
import { Utils } from '../../utils/utils';
|
||||||
|
import RepoTag from '../../models/repo-tag';
|
||||||
|
import toaster from '../toast';
|
||||||
|
|
||||||
|
export default class TagListFooter extends Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
repoID: PropTypes.string.isRequired,
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
repotagList: PropTypes.array.isRequired,
|
||||||
|
updateTags: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
showTooltip: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTooltip = () => {
|
||||||
|
this.setState({showTooltip: !this.state.showTooltip});
|
||||||
|
};
|
||||||
|
|
||||||
|
onClickImport = () => {
|
||||||
|
this.importOptionsInput.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
importTagsInputChange = () => {
|
||||||
|
if (!this.importOptionsInput.files || !this.importOptionsInput.files.length) {
|
||||||
|
toaster.warning(gettext('Please select a file'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.onload = this.onImportTags.bind(this);
|
||||||
|
fileReader.onerror = this.onImportTagsError.bind(this);
|
||||||
|
fileReader.readAsText(this.importOptionsInput.files[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
getValidTags = (tags) => {
|
||||||
|
let validTags = [];
|
||||||
|
let tagNameMap = {};
|
||||||
|
this.props.repotagList.forEach(tag => tagNameMap[tag.name] = true);
|
||||||
|
for (let i = 0; i < tags.length; i++) {
|
||||||
|
if (!tags[i] || typeof tags[i] !== 'object' || !tags[i].name || !tags[i].color) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!tagNameMap[tags[i].name]) {
|
||||||
|
validTags.push(
|
||||||
|
{
|
||||||
|
name: tags[i].name,
|
||||||
|
color: tags[i].color,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
tagNameMap[tags[i].name] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return validTags;
|
||||||
|
};
|
||||||
|
|
||||||
|
onImportTags = (event) => {
|
||||||
|
let tags = [];
|
||||||
|
try {
|
||||||
|
tags = JSON.parse(event.target.result); // handle JSON file format is error
|
||||||
|
} catch (error) {
|
||||||
|
toaster.danger(gettext('The imported tags are invalid'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(tags) || tags.length === 0) {
|
||||||
|
toaster.danger(gettext('The imported tags are invalid'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let validTags = this.getValidTags(tags);
|
||||||
|
if (validTags.length === 0) {
|
||||||
|
toaster.warning(gettext('The imported tag already exists'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
seafileAPI.createRepoTags(this.props.repoID, validTags).then((res) => {
|
||||||
|
toaster.success(gettext('Tags imported'));
|
||||||
|
let repotagList = [];
|
||||||
|
res.data.repo_tags.forEach(item => {
|
||||||
|
let repo_tag = new RepoTag(item);
|
||||||
|
repotagList.push(repo_tag);
|
||||||
|
});
|
||||||
|
this.props.updateTags(repotagList);
|
||||||
|
}).catch(error => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
this.importOptionsInput.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
onImportTagsError = () => {
|
||||||
|
toaster.success(gettext('Failed to import tags. Please reupload.'));
|
||||||
|
};
|
||||||
|
|
||||||
|
getDownloadUrl = () => {
|
||||||
|
const tags = this.props.repotagList.map(item => {
|
||||||
|
return { name: item.name, color: item.color };
|
||||||
|
});
|
||||||
|
return `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(tags))}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="tag-list-footer">
|
||||||
|
<span className="fa fa-question-circle mr-2" style={{color: '#999'}} id="import-export-tags-tip"></span>
|
||||||
|
<Tooltip
|
||||||
|
toggle={this.toggleTooltip}
|
||||||
|
delay={{show: 0, hide: 0}}
|
||||||
|
target='import-export-tags-tip'
|
||||||
|
placement='bottom'
|
||||||
|
isOpen={this.state.showTooltip}
|
||||||
|
>
|
||||||
|
{gettext('Use the import/export function to transfer tags quickly to another library. (The export is in JSON format.)')}
|
||||||
|
</Tooltip>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={ref => this.importOptionsInput = ref}
|
||||||
|
accept='.json'
|
||||||
|
className="d-none"
|
||||||
|
onChange={this.importTagsInputChange}
|
||||||
|
/>
|
||||||
|
<span className="item-text" onClick={this.onClickImport}>{gettext('Import tags')}</span>
|
||||||
|
<span className="mx-2">|</span>
|
||||||
|
<a href={this.getDownloadUrl()} download='tags.json' onClick={this.props.toggle}>
|
||||||
|
<span className="item-text">{gettext('Export tags')}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
65
frontend/src/components/popover/tag-list-item.js
Normal file
65
frontend/src/components/popover/tag-list-item.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { gettext } from '../../utils/constants';
|
||||||
|
import TagColor from '../dialog/tag-color';
|
||||||
|
import TagName from '../dialog/tag-name';
|
||||||
|
|
||||||
|
import '../../css/repo-tag.css';
|
||||||
|
import './list-tag-popover.css';
|
||||||
|
|
||||||
|
const tagListItemPropTypes = {
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
repoID: PropTypes.string.isRequired,
|
||||||
|
onDeleteTag : PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
class TagListItem extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isTagHighlighted: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseOver = () => {
|
||||||
|
this.setState({
|
||||||
|
isTagHighlighted: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMouseOut = () => {
|
||||||
|
this.setState({
|
||||||
|
isTagHighlighted: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteTag = () => {
|
||||||
|
this.props.onDeleteTag(this.props.item);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isTagHighlighted } = this.state;
|
||||||
|
const { item, repoID } = this.props;
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className={`tag-list-item px-4 d-flex justify-content-between align-items-center ${isTagHighlighted ? 'hl' : ''}`}
|
||||||
|
onMouseOver={this.onMouseOver}
|
||||||
|
onMouseOut={this.onMouseOut}
|
||||||
|
>
|
||||||
|
<TagColor repoID={repoID} tag={item} />
|
||||||
|
<TagName repoID={repoID} tag={item} />
|
||||||
|
<button
|
||||||
|
className={`tag-delete-icon sf2-icon-delete border-0 px-0 bg-transparent cursor-pointer ${isTagHighlighted ? '' : 'invisible'}`}
|
||||||
|
onClick={this.deleteTag}
|
||||||
|
aria-label={gettext('Delete')}
|
||||||
|
title={gettext('Delete')}
|
||||||
|
></button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TagListItem.propTypes = tagListItemPropTypes;
|
||||||
|
|
||||||
|
export default TagListItem;
|
96
frontend/src/components/popover/virtual-tag-color.js
Normal file
96
frontend/src/components/popover/virtual-tag-color.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Popover, PopoverBody } from 'reactstrap';
|
||||||
|
import { TAG_COLORS } from '../../constants';
|
||||||
|
|
||||||
|
import '../../css/repo-tag.css';
|
||||||
|
|
||||||
|
export default class VirtualTagColor extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
updateVirtualTag: PropTypes.func.isRequired,
|
||||||
|
tag: PropTypes.object.isRequired,
|
||||||
|
repoID: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
tagColor: this.props.tag.color,
|
||||||
|
isPopoverOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
|
if (nextProps.tag.color !== this.props.tag.color) {
|
||||||
|
this.setState({
|
||||||
|
tagColor: nextProps.tag.color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePopover = () => {
|
||||||
|
this.setState({
|
||||||
|
isPopoverOpen: !this.state.isPopoverOpen
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
selectTagColor = (e) => {
|
||||||
|
const newColor = e.target.value;
|
||||||
|
this.props.updateVirtualTag(this.props.tag, { color: newColor });
|
||||||
|
this.setState({
|
||||||
|
tagColor: newColor,
|
||||||
|
isPopoverOpen: !this.state.isPopoverOpen,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isPopoverOpen, tagColor } = this.state;
|
||||||
|
const { tag } = this.props;
|
||||||
|
const { id, color } = tag;
|
||||||
|
|
||||||
|
let colorList = [...TAG_COLORS];
|
||||||
|
// for color from previous color options
|
||||||
|
if (colorList.indexOf(color) == -1) {
|
||||||
|
colorList.unshift(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
id={`tag-${id}-color`}
|
||||||
|
className="tag-color cursor-pointer rounded-circle d-flex align-items-center justify-content-center"
|
||||||
|
style={{backgroundColor: tagColor}}
|
||||||
|
onClick={this.togglePopover}
|
||||||
|
>
|
||||||
|
<i className="fas fa-caret-down text-white"></i>
|
||||||
|
</span>
|
||||||
|
<Popover
|
||||||
|
target={`tag-${id}-color`}
|
||||||
|
isOpen={isPopoverOpen}
|
||||||
|
placement="bottom"
|
||||||
|
toggle={this.togglePopover}
|
||||||
|
className="tag-color-popover mw-100"
|
||||||
|
>
|
||||||
|
<PopoverBody className="p-2">
|
||||||
|
<div className="d-flex justify-content-between">
|
||||||
|
{colorList.map((item, index)=>{
|
||||||
|
return (
|
||||||
|
<div key={index} className="tag-color-option mx-1">
|
||||||
|
<label className="colorinput">
|
||||||
|
<input name="color" type="radio" value={item} className="colorinput-input" defaultChecked={item == tagColor} onClick={this.selectTagColor} />
|
||||||
|
<span className="colorinput-color rounded-circle d-flex align-items-center justify-content-center" style={{backgroundColor: item}}>
|
||||||
|
<i className="fas fa-check color-selected"></i>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</PopoverBody>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
58
frontend/src/components/popover/virtual-tag-list-item.js
Normal file
58
frontend/src/components/popover/virtual-tag-list-item.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { gettext } from '../../utils/constants';
|
||||||
|
import VirtualTagColor from './virtual-tag-color';
|
||||||
|
import VirtualTagName from './virtual-tag-name';
|
||||||
|
|
||||||
|
import '../../css/repo-tag.css';
|
||||||
|
import './list-tag-popover.css';
|
||||||
|
|
||||||
|
export default class VirtualTagListItem extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
repoID: PropTypes.string.isRequired,
|
||||||
|
deleteVirtualTag: PropTypes.func.isRequired,
|
||||||
|
updateVirtualTag: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isTagHighlighted: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseOver = () => {
|
||||||
|
this.setState({ isTagHighlighted: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onMouseOut = () => {
|
||||||
|
this.setState({ isTagHighlighted: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteVirtualTag = () => {
|
||||||
|
this.props.deleteVirtualTag(this.props.item);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isTagHighlighted } = this.state;
|
||||||
|
const { item, repoID } = this.props;
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className={`tag-list-item px-4 d-flex justify-content-between align-items-center ${isTagHighlighted ? 'hl' : ''}`}
|
||||||
|
onMouseOver={this.onMouseOver}
|
||||||
|
onMouseOut={this.onMouseOut}
|
||||||
|
>
|
||||||
|
<VirtualTagColor repoID={repoID} tag={item} updateVirtualTag={this.props.updateVirtualTag} />
|
||||||
|
<VirtualTagName repoID={repoID} tag={item} updateVirtualTag={this.props.updateVirtualTag} />
|
||||||
|
<button
|
||||||
|
className={`tag-delete-icon sf2-icon-delete border-0 px-0 bg-transparent cursor-pointer ${isTagHighlighted ? '' : 'invisible'}`}
|
||||||
|
onClick={this.deleteVirtualTag}
|
||||||
|
aria-label={gettext('Delete')}
|
||||||
|
title={gettext('Delete')}
|
||||||
|
></button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
89
frontend/src/components/popover/virtual-tag-name.js
Normal file
89
frontend/src/components/popover/virtual-tag-name.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import '../../css/repo-tag.css';
|
||||||
|
|
||||||
|
export default class VirtualTagName extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
updateVirtualTag: PropTypes.func.isRequired,
|
||||||
|
tag: PropTypes.object.isRequired,
|
||||||
|
repoID: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
tagName: this.props.tag.name,
|
||||||
|
isEditing: true,
|
||||||
|
};
|
||||||
|
this.input = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
|
if (nextProps.tag.name !== this.props.tag.name) {
|
||||||
|
this.setState({
|
||||||
|
tagName: nextProps.tag.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.input.current.focus();
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMode = () => {
|
||||||
|
this.setState({
|
||||||
|
isEditing: !this.state.isEditing
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
updateTagName = (e) => {
|
||||||
|
const newName = e.target.value;
|
||||||
|
this.props.updateVirtualTag(this.props.tag, { name: newName });
|
||||||
|
this.setState({
|
||||||
|
tagName: newName
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onInputKeyDown = (e) => {
|
||||||
|
if (e.key == 'Enter') {
|
||||||
|
this.toggleMode();
|
||||||
|
this.updateTagName(e);
|
||||||
|
}
|
||||||
|
else if (e.key == 'Escape') {
|
||||||
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
|
this.toggleMode();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onInputBlur = (e) => {
|
||||||
|
this.toggleMode();
|
||||||
|
this.updateTagName(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isEditing, tagName } = this.state;
|
||||||
|
return (
|
||||||
|
<div className="mx-2 flex-fill d-flex">
|
||||||
|
{isEditing ?
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
ref={this.input}
|
||||||
|
defaultValue={tagName}
|
||||||
|
onBlur={this.onInputBlur}
|
||||||
|
onKeyDown={this.onInputKeyDown}
|
||||||
|
className="flex-fill form-control-sm form-control"
|
||||||
|
/> :
|
||||||
|
<span
|
||||||
|
onClick={this.toggleMode}
|
||||||
|
className="cursor-pointer flex-fill"
|
||||||
|
style={{width: 100, height: 20}}
|
||||||
|
>{tagName}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -105,7 +105,7 @@ class Alert extends React.PureComponent {
|
|||||||
|
|
||||||
Alert.propTypes = {
|
Alert.propTypes = {
|
||||||
onRemove: PropTypes.func.isRequired,
|
onRemove: PropTypes.func.isRequired,
|
||||||
children: PropTypes.any.isRequired,
|
children: PropTypes.any,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
intent: PropTypes.string.isRequired,
|
intent: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
@@ -19,8 +19,8 @@ const propTypes = {
|
|||||||
currentRepoInfo: PropTypes.object,
|
currentRepoInfo: PropTypes.object,
|
||||||
selectedDirentList: PropTypes.array,
|
selectedDirentList: PropTypes.array,
|
||||||
onItemsMove: PropTypes.func,
|
onItemsMove: PropTypes.func,
|
||||||
posX: PropTypes.number.isRequired,
|
posX: PropTypes.number,
|
||||||
posY: PropTypes.number.isRequired,
|
posY: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PADDING_LEFT = 20;
|
const PADDING_LEFT = 20;
|
||||||
|
@@ -58,3 +58,22 @@
|
|||||||
.tag-color-option .colorinput-input:checked ~ .colorinput-color .color-selected {
|
.tag-color-option .colorinput-input:checked ~ .colorinput-color .color-selected {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* tag-color */
|
||||||
|
.tag-color-popover .popover {
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-color-popover .tag-color {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-color-popover .colorinput-color {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-color-popover .tag-color-option .colorinput-input:checked ~ .colorinput-color .color-selected {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
@@ -87,8 +87,8 @@ const propTypes = {
|
|||||||
onListContainerScroll: PropTypes.func.isRequired,
|
onListContainerScroll: PropTypes.func.isRequired,
|
||||||
onDirentClick: PropTypes.func.isRequired,
|
onDirentClick: PropTypes.func.isRequired,
|
||||||
direntDetailPanelTab: PropTypes.string,
|
direntDetailPanelTab: PropTypes.string,
|
||||||
loadDirentList: PropTypes.func.isRequired,
|
loadDirentList: PropTypes.func,
|
||||||
fullDirentList: PropTypes.array.isRequired,
|
fullDirentList: PropTypes.array,
|
||||||
};
|
};
|
||||||
|
|
||||||
class LibContentContainer extends React.Component {
|
class LibContentContainer extends React.Component {
|
||||||
|
@@ -86,8 +86,6 @@ class RichMarkdownEditor extends React.Component {
|
|||||||
editorApi={this.props.editorApi}
|
editorApi={this.props.editorApi}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
resetRichValue={this.props.resetRichValue}
|
resetRichValue={this.props.resetRichValue}
|
||||||
isSupportComment={false}
|
|
||||||
onAddComment={() => {}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={`markdown-help-wrapper ${isShowHelpWrapper ? 'show' : ''}`} style={helpWrapperStyle}>
|
<div className={`markdown-help-wrapper ${isShowHelpWrapper ? 'show' : ''}`} style={helpWrapperStyle}>
|
||||||
|
@@ -117,6 +117,54 @@ class RepoTagsView(APIView):
|
|||||||
|
|
||||||
return Response({"repo_tag": repo_tag.to_dict()}, status=status.HTTP_201_CREATED)
|
return Response({"repo_tag": repo_tag.to_dict()}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def put(self, request, repo_id):
|
||||||
|
"""bulk add repo_tags.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# argument check
|
||||||
|
tags = request.data.get('tags')
|
||||||
|
if not tags:
|
||||||
|
error_msg = 'tags invalid.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
# resource check
|
||||||
|
repo = seafile_api.get_repo(repo_id)
|
||||||
|
if not repo:
|
||||||
|
error_msg = 'Library %s not found.' % repo_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
# permission check
|
||||||
|
if check_folder_permission(request, repo_id, '/') != PERMISSION_READ_WRITE:
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
|
||||||
|
tag_objs = list()
|
||||||
|
try:
|
||||||
|
for tag in tags:
|
||||||
|
name = tag.get('name' ,'')
|
||||||
|
color = tag.get('color', '')
|
||||||
|
if name and color:
|
||||||
|
obj = RepoTags(repo_id=repo_id, name=name, color=color)
|
||||||
|
tag_objs.append(obj)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'tags invalid.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
repo_tag_list = RepoTags.objects.bulk_create(tag_objs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
repo_tags = list()
|
||||||
|
for repo_tag in repo_tag_list:
|
||||||
|
res = repo_tag.to_dict()
|
||||||
|
repo_tags.append(res)
|
||||||
|
|
||||||
|
return Response({"repo_tags": repo_tags}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class RepoTagView(APIView):
|
class RepoTagView(APIView):
|
||||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
Reference in New Issue
Block a user