From e90a64cc90b01d52c0d9050e841a36491ca20c04 Mon Sep 17 00:00:00 2001 From: Michael An <2331806369@qq.com> Date: Mon, 9 Oct 2023 21:27:44 +0800 Subject: [PATCH] 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> --- .../src/components/cur-dir-path/dir-tool.js | 171 ++++++++---------- .../src/components/dialog/list-tag-dialog.js | 158 ---------------- frontend/src/components/dialog/tag-color.js | 12 +- frontend/src/components/dialog/tag-name.js | 12 ++ .../dirent-grid-view/dirent-grid-view.js | 4 +- .../dirent-list-view/dirent-list-item.js | 2 +- .../components/popover/list-tag-popover.css | 31 ++++ .../components/popover/list-tag-popover.js | 154 ++++++++++++++++ .../src/components/popover/tag-list-footer.js | 137 ++++++++++++++ .../src/components/popover/tag-list-item.js | 65 +++++++ .../components/popover/virtual-tag-color.js | 96 ++++++++++ .../popover/virtual-tag-list-item.js | 58 ++++++ .../components/popover/virtual-tag-name.js | 89 +++++++++ frontend/src/components/toast/alert.js | 2 +- .../src/components/tree-view/tree-view.js | 4 +- frontend/src/css/repo-tag.css | 19 ++ .../lib-content-view/lib-content-container.js | 4 +- .../rich-markdown-editor/index.js | 2 - seahub/api2/endpoints/repo_tags.py | 48 +++++ 19 files changed, 802 insertions(+), 266 deletions(-) delete mode 100644 frontend/src/components/dialog/list-tag-dialog.js create mode 100644 frontend/src/components/popover/list-tag-popover.css create mode 100644 frontend/src/components/popover/list-tag-popover.js create mode 100644 frontend/src/components/popover/tag-list-footer.js create mode 100644 frontend/src/components/popover/tag-list-item.js create mode 100644 frontend/src/components/popover/virtual-tag-color.js create mode 100644 frontend/src/components/popover/virtual-tag-list-item.js create mode 100644 frontend/src/components/popover/virtual-tag-name.js diff --git a/frontend/src/components/cur-dir-path/dir-tool.js b/frontend/src/components/cur-dir-path/dir-tool.js index b3c5cc8128d..3b02df4286a 100644 --- a/frontend/src/components/cur-dir-path/dir-tool.js +++ b/frontend/src/components/cur-dir-path/dir-tool.js @@ -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 { Utils } from '../../utils/utils'; -import PropTypes from 'prop-types'; -import ModalPortal from '../modal-portal'; -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'; +import SeahubPopover from '../common/seahub-popover'; +import ListTagPopover from '../popover/list-tag-popover'; const propTypes = { repoID: PropTypes.string.isRequired, @@ -21,112 +19,93 @@ class DirTool extends React.Component { constructor(props) { super(props); this.state = { - isRepoTagDialogShow: false, - currentTag: null, isListRepoTagShow: false, - isCreateRepoTagShow: false, - isListTaggedFileShow: false, }; + this.tagsIconID = `tags-icon-${uuidv4()}`; } onShowListRepoTag = (e) => { - e.preventDefault(); - this.setState({ - isRepoTagDialogShow: true, - isListRepoTagShow: true, - isCreateRepoTagShow: false, - isListTaggedFileShow: false - }); + this.setState({ isListRepoTagShow: true }); }; - onCloseRepoTagDialog = () => { - this.setState({ - isRepoTagDialogShow: false, - isListRepoTagShow: false, - isCreateRepoTagShow: false, - isListTaggedFileShow: false - }); - }; - - onCreateRepoTagToggle = () => { - this.setState({ - isCreateRepoTagShow: !this.state.isCreateRepoTagShow, - isListRepoTagShow: !this.state.isListRepoTagShow, - }); + hidePopover = (e) => { + if (e) { + let dom = e.target; + while (dom) { + if (typeof dom.className === 'string' && dom.className.includes('tag-color-popover')) return; + dom = dom.parentNode; + } + } + this.setState({ isListRepoTagShow: false }); }; - onListTaggedFileToggle = (currentTag) => { - this.setState({ - currentTag: currentTag, - isListRepoTagShow: !this.state.isListRepoTagShow, - isListTaggedFileShow: !this.state.isListTaggedFileShow, - }); + toggleCancel = () => { + this.setState({ isListRepoTagShow: false }); }; isMarkdownFile(filePath) { - let name = Utils.getFileName(filePath); - return name.indexOf('.md') > -1 ? true : false; + return Utils.getFileName(filePath).includes('.md'); } render() { let { repoID, userPerm, currentPath } = this.props; - let isFile = this.isMarkdownFile(currentPath); - let name = Utils.getFileName(currentPath); - let trashUrl = siteRoot + 'repo/' + repoID + '/trash/'; - let historyUrl = siteRoot + 'repo/history/' + repoID + '/'; - if (userPerm === 'rw') { - if (!isFile) { - if (name) { // name not '' is not root path - trashUrl = siteRoot + 'repo/' + repoID + '/trash/?path=' + encodeURIComponent(currentPath); - return ( - - ); - } else { // currentPath === '/' is root path - return ( - - - - {this.state.isRepoTagDialogShow && ( - - - {this.state.isListRepoTagShow && ( - - )} - {this.state.isCreateRepoTagShow && ( - - )} - {this.state.isListTaggedFileShow && ( - - )} - - - )} - - ); - } - } + if (userPerm !== 'rw') { + return ''; } - return ''; + if (this.isMarkdownFile(currentPath)) { + return ''; + } + let toolbarDom = null; + if (Utils.getFileName(currentPath)) { // name not '' is not root path + let trashUrl = siteRoot + 'repo/' + repoID + '/trash/?path=' + encodeURIComponent(currentPath); + toolbarDom = ( + + ); + } else { // currentPath === '/' is root path + let trashUrl = siteRoot + 'repo/' + repoID + '/trash/'; + let historyUrl = siteRoot + 'repo/history/' + repoID + '/'; + toolbarDom = ( + + ); + } + return ( + <> + {toolbarDom} + {this.state.isListRepoTagShow && + + + + } + + ); } } diff --git a/frontend/src/components/dialog/list-tag-dialog.js b/frontend/src/components/dialog/list-tag-dialog.js deleted file mode 100644 index 8b85874ab2d..00000000000 --- a/frontend/src/components/dialog/list-tag-dialog.js +++ /dev/null @@ -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 ( -
  • - - - -
  • - ); - } -} - -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 ( - - {gettext('Tags')} - - - - - {gettext('Create a new tag')} - - - - - - - ); - } -} - -ListTagDialog.propTypes = listTagPropTypes; - -export default ListTagDialog; diff --git a/frontend/src/components/dialog/tag-color.js b/frontend/src/components/dialog/tag-color.js index ddaed712a61..124f97b4e20 100644 --- a/frontend/src/components/dialog/tag-color.js +++ b/frontend/src/components/dialog/tag-color.js @@ -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 = () => { this.setState({ isPopoverOpen: !this.state.isPopoverOpen @@ -59,7 +67,7 @@ class TagColor extends React.Component {
    @@ -70,7 +78,7 @@ class TagColor extends React.Component { isOpen={isPopoverOpen} placement="bottom" toggle={this.togglePopover} - className="mw-100" + className="tag-color-popover mw-100" >
    diff --git a/frontend/src/components/dialog/tag-name.js b/frontend/src/components/dialog/tag-name.js index c0fb4e78919..1fbc1061507 100644 --- a/frontend/src/components/dialog/tag-name.js +++ b/frontend/src/components/dialog/tag-name.js @@ -22,6 +22,14 @@ class TagName extends React.Component { this.input = React.createRef(); } + UNSAFE_componentWillReceiveProps(nextProps) { + if (nextProps.tag.name !== this.props.tag.name) { + this.setState({ + tagName: nextProps.tag.name, + }); + } + } + toggleMode = () => { this.setState({ isEditing: !this.state.isEditing @@ -51,6 +59,10 @@ class TagName extends React.Component { this.toggleMode(); this.updateTagName(e); } + else if (e.key == 'Escape') { + e.nativeEvent.stopImmediatePropagation(); + this.toggleMode(); + } }; onInputBlur = (e) => { diff --git a/frontend/src/components/dirent-grid-view/dirent-grid-view.js b/frontend/src/components/dirent-grid-view/dirent-grid-view.js index e40b92efcc0..4c02422f7da 100644 --- a/frontend/src/components/dirent-grid-view/dirent-grid-view.js +++ b/frontend/src/components/dirent-grid-view/dirent-grid-view.js @@ -48,8 +48,8 @@ const propTypes = { onAddFolder: PropTypes.func.isRequired, showDirentDetail: PropTypes.func.isRequired, onItemRename: PropTypes.func.isRequired, - posX: PropTypes.number.isRequired, - posY: PropTypes.number.isRequired, + posX: PropTypes.number, + posY: PropTypes.number, }; class DirentGridView extends React.Component { diff --git a/frontend/src/components/dirent-list-view/dirent-list-item.js b/frontend/src/components/dirent-list-view/dirent-list-item.js index dfab5b9bb8f..b7f2a62749a 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-item.js +++ b/frontend/src/components/dirent-list-view/dirent-list-item.js @@ -54,7 +54,7 @@ const propTypes = { showDirentDetail: PropTypes.func.isRequired, onItemsMove: PropTypes.func.isRequired, onShowDirentsDraggablePreview: PropTypes.func, - loadDirentList: PropTypes.func.isRequired, + loadDirentList: PropTypes.func, }; class DirentListItem extends React.Component { diff --git a/frontend/src/components/popover/list-tag-popover.css b/frontend/src/components/popover/list-tag-popover.css new file mode 100644 index 00000000000..0ef871a2ed0 --- /dev/null +++ b/frontend/src/components/popover/list-tag-popover.css @@ -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; +} diff --git a/frontend/src/components/popover/list-tag-popover.js b/frontend/src/components/popover/list-tag-popover.js new file mode 100644 index 00000000000..3eadd4c2b68 --- /dev/null +++ b/frontend/src/components/popover/list-tag-popover.js @@ -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 ( + +
      + {this.state.repotagList.map((repoTag, index) => { + if (repoTag.is_virtual) { + return ( + + ); + } else { + return ( + + ); + } + })} +
    +
    + {gettext('Create a new tag')} +
    + +
    + ); + } +} diff --git a/frontend/src/components/popover/tag-list-footer.js b/frontend/src/components/popover/tag-list-footer.js new file mode 100644 index 00000000000..e250aeecee2 --- /dev/null +++ b/frontend/src/components/popover/tag-list-footer.js @@ -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 ( +
    + + + {gettext('Use the import/export function to transfer tags quickly to another library. (The export is in JSON format.)')} + + this.importOptionsInput = ref} + accept='.json' + className="d-none" + onChange={this.importTagsInputChange} + /> + {gettext('Import tags')} + | + + {gettext('Export tags')} + +
    + ); + } +} diff --git a/frontend/src/components/popover/tag-list-item.js b/frontend/src/components/popover/tag-list-item.js new file mode 100644 index 00000000000..69109e4a2cd --- /dev/null +++ b/frontend/src/components/popover/tag-list-item.js @@ -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 ( +
  • + + + +
  • + ); + } +} + +TagListItem.propTypes = tagListItemPropTypes; + +export default TagListItem; diff --git a/frontend/src/components/popover/virtual-tag-color.js b/frontend/src/components/popover/virtual-tag-color.js new file mode 100644 index 00000000000..74348884890 --- /dev/null +++ b/frontend/src/components/popover/virtual-tag-color.js @@ -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 ( +
    + + + + + +
    + {colorList.map((item, index)=>{ + return ( +
    + +
    + ); + }) + } +
    +
    +
    +
    + ); + } +} diff --git a/frontend/src/components/popover/virtual-tag-list-item.js b/frontend/src/components/popover/virtual-tag-list-item.js new file mode 100644 index 00000000000..6334c4ff4da --- /dev/null +++ b/frontend/src/components/popover/virtual-tag-list-item.js @@ -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 ( +
  • + + + +
  • + ); + } +} diff --git a/frontend/src/components/popover/virtual-tag-name.js b/frontend/src/components/popover/virtual-tag-name.js new file mode 100644 index 00000000000..e22b909e800 --- /dev/null +++ b/frontend/src/components/popover/virtual-tag-name.js @@ -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 ( +
    + {isEditing ? + : + {tagName} + } +
    + ); + } +} diff --git a/frontend/src/components/toast/alert.js b/frontend/src/components/toast/alert.js index dbfe69ef45d..7e51ad501a3 100644 --- a/frontend/src/components/toast/alert.js +++ b/frontend/src/components/toast/alert.js @@ -105,7 +105,7 @@ class Alert extends React.PureComponent { Alert.propTypes = { onRemove: PropTypes.func.isRequired, - children: PropTypes.any.isRequired, + children: PropTypes.any, title: PropTypes.string.isRequired, intent: PropTypes.string.isRequired, }; diff --git a/frontend/src/components/tree-view/tree-view.js b/frontend/src/components/tree-view/tree-view.js index f0b8393f0ff..4812080d60a 100644 --- a/frontend/src/components/tree-view/tree-view.js +++ b/frontend/src/components/tree-view/tree-view.js @@ -19,8 +19,8 @@ const propTypes = { currentRepoInfo: PropTypes.object, selectedDirentList: PropTypes.array, onItemsMove: PropTypes.func, - posX: PropTypes.number.isRequired, - posY: PropTypes.number.isRequired, + posX: PropTypes.number, + posY: PropTypes.number, }; const PADDING_LEFT = 20; diff --git a/frontend/src/css/repo-tag.css b/frontend/src/css/repo-tag.css index e5f5f588744..76381988e91 100644 --- a/frontend/src/css/repo-tag.css +++ b/frontend/src/css/repo-tag.css @@ -58,3 +58,22 @@ .tag-color-option .colorinput-input:checked ~ .colorinput-color .color-selected { 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; +} diff --git a/frontend/src/pages/lib-content-view/lib-content-container.js b/frontend/src/pages/lib-content-view/lib-content-container.js index 1d7bee64ed7..dc753ab001e 100644 --- a/frontend/src/pages/lib-content-view/lib-content-container.js +++ b/frontend/src/pages/lib-content-view/lib-content-container.js @@ -87,8 +87,8 @@ const propTypes = { onListContainerScroll: PropTypes.func.isRequired, onDirentClick: PropTypes.func.isRequired, direntDetailPanelTab: PropTypes.string, - loadDirentList: PropTypes.func.isRequired, - fullDirentList: PropTypes.array.isRequired, + loadDirentList: PropTypes.func, + fullDirentList: PropTypes.array, }; class LibContentContainer extends React.Component { diff --git a/frontend/src/pages/markdown-editor/rich-markdown-editor/index.js b/frontend/src/pages/markdown-editor/rich-markdown-editor/index.js index 90fb0917ac2..0f35fc7365e 100644 --- a/frontend/src/pages/markdown-editor/rich-markdown-editor/index.js +++ b/frontend/src/pages/markdown-editor/rich-markdown-editor/index.js @@ -86,8 +86,6 @@ class RichMarkdownEditor extends React.Component { editorApi={this.props.editorApi} onChange={this.props.onChange} resetRichValue={this.props.resetRichValue} - isSupportComment={false} - onAddComment={() => {}} />
    diff --git a/seahub/api2/endpoints/repo_tags.py b/seahub/api2/endpoints/repo_tags.py index 3871ca5c084..f54038fee7c 100644 --- a/seahub/api2/endpoints/repo_tags.py +++ b/seahub/api2/endpoints/repo_tags.py @@ -117,6 +117,54 @@ def post(self, request, repo_id): 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): authentication_classes = (TokenAuthentication, SessionAuthentication)