mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-03 07:55:36 +00:00
[tags] redesigned 'Tags' dialog
- redesigned 'tag color edit', 'tag name edit', 'tag delete'
This commit is contained in:
@@ -83,14 +83,14 @@ class TagItem extends React.Component {
|
||||
const isTagSelected = repoTagIdList.indexOf(repoTag.id) != -1;
|
||||
return (
|
||||
<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}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
<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-name">{repoTag.name}</span>
|
||||
<span className="tag-name mx-2">{repoTag.name}</span>
|
||||
</div>
|
||||
{isTagSelected && <i className="fas fa-check tag-selected-icon"></i>}
|
||||
</li>
|
||||
|
@@ -6,13 +6,15 @@ 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,
|
||||
onTagUpdate: PropTypes.func.isRequired,
|
||||
onListTaggedFiles: PropTypes.func.isRequired,
|
||||
repoID: PropTypes.string.isRequired,
|
||||
onDeleteTag : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class TagListItem extends React.Component {
|
||||
@@ -20,45 +22,43 @@ class TagListItem extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showSelectedTag: false
|
||||
isTagHighlighted: false
|
||||
};
|
||||
}
|
||||
|
||||
onMouseOver = () => {
|
||||
this.setState({
|
||||
showSelectedTag: true
|
||||
isTagHighlighted: true
|
||||
});
|
||||
}
|
||||
|
||||
onMouseOut = () => {
|
||||
this.setState({
|
||||
showSelectedTag: false
|
||||
isTagHighlighted: false
|
||||
});
|
||||
}
|
||||
|
||||
onTagUpdate = () => {
|
||||
this.props.onTagUpdate(this.props.item);
|
||||
}
|
||||
|
||||
onListTaggedFiles = () => {
|
||||
this.props.onListTaggedFiles(this.props.item);
|
||||
deleteTag = () => {
|
||||
this.props.onDeleteTag(this.props.item);
|
||||
}
|
||||
|
||||
render() {
|
||||
let color = this.props.item.color;
|
||||
let drakColor = Utils.getDarkColor(color);
|
||||
const fileCount = this.props.item.fileCount;
|
||||
let fileTranslation = (fileCount === 1 || fileCount === 0) ? gettext('file') : gettext('files');
|
||||
const { isTagHighlighted } = this.state;
|
||||
const { item, repoID } = this.props;
|
||||
return (
|
||||
<li className="tag-list-item">
|
||||
<div className="tag-demo" style={{backgroundColor:color}} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
<span className={`${this.state.showSelectedTag ? 'show-tag-selected': ''}`} style={{backgroundColor: drakColor}}></span>
|
||||
<span className="tag-name">{this.props.item.name}</span>
|
||||
<button className="tag-files border-0 bg-transparent" onClick={this.onListTaggedFiles}>
|
||||
{fileCount}{' '}{fileTranslation}
|
||||
</button>
|
||||
</div>
|
||||
<button className="tag-edit fa fa-pencil-alt border-0" onClick={this.onTagUpdate} aria-label={gettext('Edit')} title={gettext('Edit')}></button>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -69,16 +69,14 @@ TagListItem.propTypes = tagListItemPropTypes;
|
||||
const listTagPropTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
onListTagCancel: PropTypes.func.isRequired,
|
||||
onCreateRepoTag: PropTypes.func.isRequired,
|
||||
onUpdateRepoTag: PropTypes.func.isRequired,
|
||||
onListTaggedFiles: PropTypes.func.isRequired,
|
||||
onCreateRepoTag: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class ListTagDialog extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
repotagList: [],
|
||||
repotagList: []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,7 +89,7 @@ class ListTagDialog extends React.Component {
|
||||
repotagList.push(repo_tag);
|
||||
});
|
||||
this.setState({
|
||||
repotagList: repotagList,
|
||||
repotagList: repotagList
|
||||
});
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
@@ -108,24 +106,44 @@ class ListTagDialog extends React.Component {
|
||||
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>
|
||||
<ModalBody className="px-0">
|
||||
<ul className="tag-list tag-list-container">
|
||||
{this.state.repotagList.map((repoTag, index) => {
|
||||
return (
|
||||
<TagListItem
|
||||
key={index}
|
||||
item={repoTag}
|
||||
onTagUpdate={this.props.onUpdateRepoTag}
|
||||
onListTaggedFiles={this.props.onListTaggedFiles}
|
||||
repoID={this.props.repoID}
|
||||
onDeleteTag={this.onDeleteTag}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
<ModalFooter>
|
||||
<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;
|
Reference in New Issue
Block a user