1
0
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:
llj
2022-02-25 18:11:54 +08:00
parent 8862328958
commit 97da4f2b63
6 changed files with 242 additions and 91 deletions

View File

@@ -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 && (

View File

@@ -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>

View File

@@ -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>

View 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;

View 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;

View File

@@ -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;