mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-15 06:44:16 +00:00
[tags] redesigned 'Tags' dialog
- redesigned 'tag color edit', 'tag name edit', 'tag delete'
This commit is contained in:
@@ -113,8 +113,6 @@ class DirTool extends React.Component {
|
|||||||
repoID={repoID}
|
repoID={repoID}
|
||||||
onListTagCancel={this.onCloseRepoTagDialog}
|
onListTagCancel={this.onCloseRepoTagDialog}
|
||||||
onCreateRepoTag={this.onCreateRepoTagToggle}
|
onCreateRepoTag={this.onCreateRepoTagToggle}
|
||||||
onUpdateRepoTag={this.onUpdateRepoTagToggle}
|
|
||||||
onListTaggedFiles={this.onListTaggedFileToggle}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{this.state.isCreateRepoTagShow && (
|
{this.state.isCreateRepoTagShow && (
|
||||||
|
@@ -83,14 +83,14 @@ class TagItem extends React.Component {
|
|||||||
const isTagSelected = repoTagIdList.indexOf(repoTag.id) != -1;
|
const isTagSelected = repoTagIdList.indexOf(repoTag.id) != -1;
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={`tag-list-item px-4 d-flex justify-content-between align-items-center ${isTagHighlighted ? 'hl' : ''}`}
|
className={`tag-list-item cursor-pointer px-4 d-flex justify-content-between align-items-center ${isTagHighlighted ? 'hl' : ''}`}
|
||||||
onClick={this.onEditFileTag}
|
onClick={this.onEditFileTag}
|
||||||
onMouseEnter={this.onMouseEnter}
|
onMouseEnter={this.onMouseEnter}
|
||||||
onMouseLeave={this.onMouseLeave}
|
onMouseLeave={this.onMouseLeave}
|
||||||
>
|
>
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<span className="tag-color w-4 h-4 rounded-circle" style={{backgroundColor: repoTag.color}}></span>
|
<span className="tag-color w-4 h-4 rounded-circle" style={{backgroundColor: repoTag.color}}></span>
|
||||||
<span className="tag-name">{repoTag.name}</span>
|
<span className="tag-name mx-2">{repoTag.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{isTagSelected && <i className="fas fa-check tag-selected-icon"></i>}
|
{isTagSelected && <i className="fas fa-check tag-selected-icon"></i>}
|
||||||
</li>
|
</li>
|
||||||
|
@@ -6,13 +6,15 @@ 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 RepoTag from '../../models/repo-tag';
|
import RepoTag from '../../models/repo-tag';
|
||||||
|
import TagColor from './tag-color';
|
||||||
|
import TagName from './tag-name';
|
||||||
|
|
||||||
import '../../css/repo-tag.css';
|
import '../../css/repo-tag.css';
|
||||||
|
|
||||||
const tagListItemPropTypes = {
|
const tagListItemPropTypes = {
|
||||||
item: PropTypes.object.isRequired,
|
item: PropTypes.object.isRequired,
|
||||||
onTagUpdate: PropTypes.func.isRequired,
|
repoID: PropTypes.string.isRequired,
|
||||||
onListTaggedFiles: PropTypes.func.isRequired,
|
onDeleteTag : PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
class TagListItem extends React.Component {
|
class TagListItem extends React.Component {
|
||||||
@@ -20,45 +22,43 @@ class TagListItem extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
showSelectedTag: false
|
isTagHighlighted: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseOver = () => {
|
onMouseOver = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
showSelectedTag: true
|
isTagHighlighted: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseOut = () => {
|
onMouseOut = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
showSelectedTag: false
|
isTagHighlighted: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onTagUpdate = () => {
|
deleteTag = () => {
|
||||||
this.props.onTagUpdate(this.props.item);
|
this.props.onDeleteTag(this.props.item);
|
||||||
}
|
|
||||||
|
|
||||||
onListTaggedFiles = () => {
|
|
||||||
this.props.onListTaggedFiles(this.props.item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let color = this.props.item.color;
|
const { isTagHighlighted } = this.state;
|
||||||
let drakColor = Utils.getDarkColor(color);
|
const { item, repoID } = this.props;
|
||||||
const fileCount = this.props.item.fileCount;
|
|
||||||
let fileTranslation = (fileCount === 1 || fileCount === 0) ? gettext('file') : gettext('files');
|
|
||||||
return (
|
return (
|
||||||
<li className="tag-list-item">
|
<li
|
||||||
<div className="tag-demo" style={{backgroundColor:color}} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
className={`tag-list-item px-4 d-flex justify-content-between align-items-center ${isTagHighlighted ? 'hl' : ''}`}
|
||||||
<span className={`${this.state.showSelectedTag ? 'show-tag-selected': ''}`} style={{backgroundColor: drakColor}}></span>
|
onMouseOver={this.onMouseOver}
|
||||||
<span className="tag-name">{this.props.item.name}</span>
|
onMouseOut={this.onMouseOut}
|
||||||
<button className="tag-files border-0 bg-transparent" onClick={this.onListTaggedFiles}>
|
>
|
||||||
{fileCount}{' '}{fileTranslation}
|
<TagColor repoID={repoID} tag={item} />
|
||||||
</button>
|
<TagName repoID={repoID} tag={item} />
|
||||||
</div>
|
<button
|
||||||
<button className="tag-edit fa fa-pencil-alt border-0" onClick={this.onTagUpdate} aria-label={gettext('Edit')} title={gettext('Edit')}></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>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -69,16 +69,14 @@ TagListItem.propTypes = tagListItemPropTypes;
|
|||||||
const listTagPropTypes = {
|
const listTagPropTypes = {
|
||||||
repoID: PropTypes.string.isRequired,
|
repoID: PropTypes.string.isRequired,
|
||||||
onListTagCancel: PropTypes.func.isRequired,
|
onListTagCancel: PropTypes.func.isRequired,
|
||||||
onCreateRepoTag: PropTypes.func.isRequired,
|
onCreateRepoTag: PropTypes.func.isRequired
|
||||||
onUpdateRepoTag: PropTypes.func.isRequired,
|
|
||||||
onListTaggedFiles: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class ListTagDialog extends React.Component {
|
class ListTagDialog extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
repotagList: [],
|
repotagList: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +89,7 @@ class ListTagDialog extends React.Component {
|
|||||||
repotagList.push(repo_tag);
|
repotagList.push(repo_tag);
|
||||||
});
|
});
|
||||||
this.setState({
|
this.setState({
|
||||||
repotagList: repotagList,
|
repotagList: repotagList
|
||||||
});
|
});
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
let errMessage = Utils.getErrorMsg(error);
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
@@ -108,24 +106,44 @@ class ListTagDialog extends React.Component {
|
|||||||
this.props.onCreateRepoTag();
|
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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ModalHeader toggle={this.toggle}>{gettext('Tags')}</ModalHeader>
|
<ModalHeader toggle={this.toggle}>{gettext('Tags')}</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody className="px-0">
|
||||||
<ul className="tag-list tag-list-container">
|
<ul className="tag-list tag-list-container">
|
||||||
{this.state.repotagList.map((repoTag, index) => {
|
{this.state.repotagList.map((repoTag, index) => {
|
||||||
return (
|
return (
|
||||||
<TagListItem
|
<TagListItem
|
||||||
key={index}
|
key={index}
|
||||||
item={repoTag}
|
item={repoTag}
|
||||||
onTagUpdate={this.props.onUpdateRepoTag}
|
repoID={this.props.repoID}
|
||||||
onListTaggedFiles={this.props.onListTaggedFiles}
|
onDeleteTag={this.onDeleteTag}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
<a href="#" className="add-tag-link" onClick={this.createNewTag}>{gettext('Create a new tag')}</a>
|
<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>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="secondary" onClick={this.toggle}>{gettext('Close')}</Button>
|
<Button color="secondary" onClick={this.toggle}>{gettext('Close')}</Button>
|
||||||
|
99
frontend/src/components/dialog/tag-color.js
Normal file
99
frontend/src/components/dialog/tag-color.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Popover, PopoverBody } from 'reactstrap';
|
||||||
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
|
import { Utils } from '../../utils/utils';
|
||||||
|
import toaster from '../toast';
|
||||||
|
|
||||||
|
import '../../css/repo-tag.css';
|
||||||
|
|
||||||
|
const tagColorPropTypes = {
|
||||||
|
tag: PropTypes.object.isRequired,
|
||||||
|
repoID: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
class TagColor extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
tagColor: this.props.tag.color,
|
||||||
|
isPopoverOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePopover = () => {
|
||||||
|
this.setState({
|
||||||
|
isPopoverOpen: !this.state.isPopoverOpen
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectTagColor = (e) => {
|
||||||
|
const newColor = e.target.value;
|
||||||
|
const { repoID, tag } = this.props;
|
||||||
|
const { id, name } = tag;
|
||||||
|
seafileAPI.updateRepoTag(repoID, id, name, newColor).then(() => {
|
||||||
|
this.setState({
|
||||||
|
tagColor: newColor,
|
||||||
|
isPopoverOpen: !this.state.isPopoverOpen
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isPopoverOpen, tagColor } = this.state;
|
||||||
|
const { tag } = this.props;
|
||||||
|
const { id, color } = tag;
|
||||||
|
|
||||||
|
let colorList = ['#FBD44A', '#EAA775', '#F4667C', '#DC82D2', '#9860E5', '#9F8CF1', '#59CB74', '#ADDF84', '#89D2EA', '#4ECCCB', '#46A1FD', '#C2C2C2'];
|
||||||
|
// 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 w-4 h-4 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="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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TagColor.propTypes = tagColorPropTypes;
|
||||||
|
|
||||||
|
export default TagColor;
|
86
frontend/src/components/dialog/tag-name.js
Normal file
86
frontend/src/components/dialog/tag-name.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
|
import { Utils } from '../../utils/utils';
|
||||||
|
import toaster from '../toast';
|
||||||
|
|
||||||
|
import '../../css/repo-tag.css';
|
||||||
|
|
||||||
|
const tagNamePropTypes = {
|
||||||
|
tag: PropTypes.object.isRequired,
|
||||||
|
repoID: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
class TagName extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
tagName: this.props.tag.name,
|
||||||
|
isEditing: false
|
||||||
|
};
|
||||||
|
this.input = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMode = () => {
|
||||||
|
this.setState({
|
||||||
|
isEditing: !this.state.isEditing
|
||||||
|
}, () => {
|
||||||
|
if (this.state.isEditing) {
|
||||||
|
this.input.current.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTagName = (e) => {
|
||||||
|
const newName = e.target.value;
|
||||||
|
const { repoID, tag } = this.props;
|
||||||
|
const { id, color } = tag;
|
||||||
|
seafileAPI.updateRepoTag(repoID, id, newName, color).then(() => {
|
||||||
|
this.setState({
|
||||||
|
tagName: newName
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onInputKeyDown = (e) => {
|
||||||
|
if (e.key == 'Enter') {
|
||||||
|
this.toggleMode();
|
||||||
|
this.updateTagName(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
>{tagName}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TagName.propTypes = tagNamePropTypes;
|
||||||
|
|
||||||
|
export default TagName;
|
@@ -16,74 +16,24 @@
|
|||||||
|
|
||||||
.tag-list-item {
|
.tag-list-item {
|
||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-list-item.hl {
|
.tag-list-item.hl {
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-color {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-selected-icon {
|
.tag-selected-icon {
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
.tag-delete-icon {
|
||||||
.tag-list-item .tag-demo {
|
color: #999;
|
||||||
flex: 1;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: #ffffff;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.show-tag-selected {
|
.tag-delete-icon:hover {
|
||||||
width: 0.5rem;
|
color: #444;
|
||||||
align-self: stretch;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-demo .tag-name {
|
|
||||||
flex: 1;
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
.tag-demo .tag-files {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-list-item .tag-edit {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
width: 2.25rem;
|
|
||||||
height: 2.25rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
color: #798d99;
|
|
||||||
cursor: pointer;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-list-item .tag-edit:hover {
|
|
||||||
color: #092d42;
|
|
||||||
background-color: rgba(9,45,66,.13);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
.tag-list-item .tag-operation {
|
|
||||||
position: absolute;
|
|
||||||
right: 0.5rem;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
.file-tag-item {
|
.file-tag-item {
|
||||||
margin: 0.25rem 0;
|
margin: 0.25rem 0;
|
||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem;
|
||||||
|
Reference in New Issue
Block a user