1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-08 02:10:24 +00:00

repo-tags-show (#2494)

This commit is contained in:
WangJianhui666
2018-11-02 15:34:34 +08:00
committed by Daniel Pan
parent 973ca9cf35
commit a63e8a4009
13 changed files with 457 additions and 39 deletions

View File

@@ -526,7 +526,7 @@
},
"axios": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
"resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
"integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
"requires": {
"follow-redirects": "^1.3.0",
@@ -1756,7 +1756,7 @@
},
"blob": {
"version": "0.0.4",
"resolved": "http://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
"integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE="
},
"bluebird": {
@@ -10226,9 +10226,9 @@
}
},
"seafile-js": {
"version": "0.2.29",
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.29.tgz",
"integrity": "sha512-fsWHcTWZk2iV/Ah3lRmnO5vyB0AFoxVyV3Z7FQ7k8aAYQtWcf07hxbFIhFpGU6cpmaq2mtnJQBUNU9zQdKtGRA==",
"version": "0.2.31",
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.31.tgz",
"integrity": "sha512-l9IXpXUaQD/MOUSyYSPR1w+Nq2fcIWiif0e3YDCWvAb1sLp2lNlDdiISkGYPoEADUJtDqQrJfKiVHgKIaj1CXQ==",
"requires": {
"axios": "^0.18.0",
"form-data": "^2.3.2"
@@ -11852,13 +11852,13 @@
"dependencies": {
"ansi-regex": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-1.1.1.tgz",
"resolved": "http://registry.npmjs.org/ansi-regex/-/ansi-regex-1.1.1.tgz",
"integrity": "sha1-QchHGUZGN15qGl0Qw8oFTvn8mA0=",
"dev": true
},
"strip-ansi": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-2.0.1.tgz",
"resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-2.0.1.tgz",
"integrity": "sha1-32LBqpTtLxFOHQ8h/R1QSCt5pg4=",
"dev": true,
"requires": {

View File

@@ -25,7 +25,7 @@
"react-dom": "^16.5.2",
"react-moment": "^0.7.9",
"reactstrap": "^6.4.0",
"seafile-js": "^0.2.29",
"seafile-js": "^0.2.31",
"seafile-ui": "^0.1.10",
"sw-precache-webpack-plugin": "0.11.4",
"unified": "^7.0.0",

View File

@@ -0,0 +1,102 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Input } from 'reactstrap';
import { gettext, repoID } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
const propTypes = {
toggleCancel: PropTypes.func.isRequired,
};
class CreateTagDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
tagName: '',
tagColor: '',
newTag: {},
colorList: ['lime', 'teal', 'azure', 'green', 'blue', 'purple', 'pink', 'indigo'],
};
this.newInput = React.createRef();
}
inputNewName = (e) => {
this.setState({
tagName: e.target.value,
});
}
selectTagcolor = (e) => {
this.setState({
tagColor: e.target.value,
});
}
createTag = () => {
let name = this.state.tagName;
let color = this.state.tagColor;
seafileAPI.createRepoTag(repoID, name, color).then(() =>{
this.props.toggleCancel();
});
}
handleKeyPress = (e) => {
if (e.key === 'Enter') {
this.createTag();
}
}
toggle = () => {
this.props.toggleCancel();
}
componentDidMount() {
this.setState({
tagColor: this.state.colorList[0]
});
this.newInput.focus();
this.newInput.setSelectionRange(0, 0);
}
render() {
let colorList = this.state.colorList;
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('New Tag')}</ModalHeader>
<ModalBody>
<div className="tag-create">
<p>{gettext('Name')}</p>
<Input onKeyPress={this.handleKeyPress} innerRef={input => {this.newInput = input;}} placeholder={gettext('name')} value={this.state.tagName} onChange={this.inputNewName}/>
<div className="form-group color-chooser">
<label className="form-label">{gettext('Select a color')}</label>
<div className="row gutters-xs">
{colorList.map((item, index)=>{
var className = 'colorinput-color bg-' + item;
return (
<div key={index} className="col-auto" onChange={this.selectTagcolor}>
<label className="colorinput">
{index===0 ?
<input name="color" type="radio" value={item} className="colorinput-input" defaultChecked onClick={this.selectTagcolor}></input> :
<input name="color" type="radio" value={item} className="colorinput-input" onClick={this.selectTagcolor}></input>}
<span className={className}></span>
</label>
</div>
);
})
}
</div>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.createTag}>{gettext('Save')}</Button>
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
</ModalFooter>
</Modal>
);
}
}
CreateTagDialog.propTypes = propTypes;
export default CreateTagDialog;

View File

@@ -19,7 +19,7 @@ class Delete extends React.Component {
let name = this.props.currentNode.name;
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Delete')}</ModalHeader>
<ModalHeader toggle={this.toggle}>{gettext('Delete Tag')}</ModalHeader>
<ModalBody>
<p>{gettext('Are you sure to delete')}{' '}<b>{name}</b> ?</p>
</ModalBody>

View File

@@ -0,0 +1,97 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import { gettext, repoID } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import RepoTag from '../../models/repo-tag';
import '../../css/repo-tag.css';
const tagListItemPropTypes = {
item: PropTypes.object.isRequired,
onTagUpdate: PropTypes.func.isRequired,
};
class TagListItem extends React.Component {
onTagUpdate = () => {
this.props.onTagUpdate(this.props.item);
}
render() {
return(
<li className="tag-list-item">
<span className="tag-demo" style={{background: this.props.item.color}}>{this.props.item.name}</span>
<i className="tag-edit fa fa-pencil" onClick={this.onTagUpdate}></i>
</li>
);
}
}
TagListItem.propTypes = tagListItemPropTypes;
const listTagPropTypes = {
onListTagCancel: PropTypes.func.isRequired,
onCreateRepoTag: PropTypes.func.isRequired,
onUpdateRepoTag: PropTypes.func.isRequired,
};
class ListTagDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
repotagList: [],
};
}
componentDidMount() {
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,
});
});
}
toggle = () => {
this.props.onListTagCancel();
}
render() {
return (
<Fragment>
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Tag List')}</ModalHeader>
<ModalBody>
{
this.state.repotagList.length === 0 &&
<div className="tag-list tag-list-container">
{gettext('Click new tag button to create tags.')}
</div>
}
{ this.state.repotagList.length > 0 &&
<ul className="tag-list tag-list-container">
{this.state.repotagList.map((repoTag, index) => {
return (
<TagListItem key={index} item={repoTag} onTagUpdate={this.props.onUpdateRepoTag}/>
);
})}
</ul>
}
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.props.onCreateRepoTag}>{gettext('New Tag')}</Button>
<Button color="secondary" onClick={this.toggle}>{gettext('Close')}</Button>
</ModalFooter>
</Modal>
</Fragment>
);
}
}
ListTagDialog.propTypes = listTagPropTypes;
export default ListTagDialog;

View File

@@ -0,0 +1,115 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Input } from 'reactstrap';
import { gettext, repoID } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
const propTypes = {
currentTag: PropTypes.object,
toggleCancel: PropTypes.func.isRequired,
};
class UpdateTagDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
deleteRepoTag: false,
newName: this.props.currentTag.name,
newColor: this.props.currentTag.color,
colorList: ['lime', 'teal', 'azure', 'green', 'blue', 'purple', 'pink', 'indigo'],
};
this.newInput = React.createRef();
}
componentDidMount() {
this.newInput.focus();
this.newInput.setSelectionRange(0, -1);
}
inputNewName = (e) => {
this.setState({
newName: e.target.value,
});
}
selectNewcolor = (e) => {
this.setState({
newColor: e.target.value,
});
}
updateTag = () => {
let tag_id = this.props.currentTag.id;
let name = this.state.newName;
let color = this.state.newColor;
seafileAPI.updateRepoTag(repoID, tag_id, name, color).then(() => {
this.props.toggleCancel();
});
}
handleKeyPress = (e) => {
if (e.key === 'Enter') {
this.updateTag();
}
}
toggle = () => {
this.props.toggleCancel();
}
deleteTagClick = (item) => {
this.setState({
deleteRepoTag: !this.state.deleteRepoTag,
});
}
onDeleteTag = () => {
let tag = this.props.currentTag;
seafileAPI.deleteRepoTag(repoID, tag.id).then(() => {
this.props.toggleCancel();
});
}
render() {
let colorList = this.state.colorList;
return (
<Fragment>
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Edit Tag')}</ModalHeader>
<ModalBody>
<div className="tag-edit">
<p>{gettext('Name:')}</p>
<Input onKeyPress={this.handleKeyPress} innerRef={input => {this.newInput = input;}} placeholder="newName" value={this.state.newName} onChange={this.inputNewName}/>
<div className="form-group color-chooser">
<label className="form-label">{gettext('Select a color')}</label>
<div className="row gutters-xs">
{colorList.map((item, index)=>{
var className = 'colorinput-color bg-' + item;
return (
<div key={index} className="col-auto" onChange={this.selectNewcolor}>
<label className="colorinput">
{item===this.props.currentTag.color ?
<input name="color" type="radio" value={item} className="colorinput-input" defaultChecked onChange={this.selectNewcolor}></input> :
<input name="color" type="radio" value={item} className="colorinput-input" onChange={this.selectNewcolor}></input>}
<span className={className}></span>
</label>
</div>
);
})}
</div>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.updateTag}>{gettext('Save')}</Button>
<Button color="danger" onClick={this.onDeleteTag}>{gettext('Delete')}</Button>
</ModalFooter>
</Modal>
</Fragment>
);
}
}
UpdateTagDialog.propTypes = propTypes;
export default UpdateTagDialog;

View File

@@ -1,7 +1,10 @@
import React from 'react';
import React, { Fragment } from 'react';
import { gettext, repoID, slug, permission, siteRoot } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import PropTypes from 'prop-types';
import ListTagDialog from '../dialog/list-tag-dialog';
import CreateTagDialog from '../dialog/create-tag-dialog';
import UpdateTagDialog from '../dialog/update-tag-dialog';
const propTypes = {
filePath: PropTypes.string.isRequired
@@ -9,6 +12,35 @@ const propTypes = {
class PathToolbar extends React.Component {
constructor(props) {
super(props);
this.state = {
currentTag: null,
isListRepoTagShow: false,
isUpdateRepoTagShow: false,
isCreateRepoTagShow: false,
};
}
onListRepoTagToggle = () => {
this.setState({isListRepoTagShow: !this.state.isListRepoTagShow});
}
onCreateRepoTagToggle = () => {
this.setState({
isCreateRepoTagShow: !this.state.isCreateRepoTagShow,
isListRepoTagShow: !this.state.isListRepoTagShow,
});
}
onUpdateRepoTagToggle = (currentTag) => {
this.setState({
currentTag: currentTag,
isListRepoTagShow: !this.state.isListRepoTagShow,
isUpdateRepoTagShow: !this.state.isUpdateRepoTagShow,
});
}
isMarkdownFile(filePath) {
let lastIndex = filePath.lastIndexOf('/');
let name = filePath.slice(lastIndex + 1);
@@ -23,10 +55,34 @@ class PathToolbar extends React.Component {
let historyUrl = siteRoot + 'repo/history/' + repoID + '/?referer=' + encodeURIComponent(location.href);
if ( (name === slug || name === '') && !isFile && permission) {
return (
<Fragment>
<ul className="path-toolbar">
<li className="toolbar-item"><a className="op-link sf2-icon-tag-manager" onClick={this.onListRepoTagToggle} title={gettext('Tags')} aria-label={gettext('Tags')}></a></li>
<li className="toolbar-item"><a className="op-link sf2-icon-trash" 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>
{
this.state.isListRepoTagShow &&
<ListTagDialog
onListTagCancel={this.onListRepoTagToggle}
onCreateRepoTag={this.onCreateRepoTagToggle}
onUpdateRepoTag={this.onUpdateRepoTagToggle}
/>
}
{
this.state.isCreateRepoTagShow &&
<CreateTagDialog
toggleCancel={this.onCreateRepoTagToggle}
/>
}
{
this.state.isUpdateRepoTagShow &&
<UpdateTagDialog
currentTag={this.state.currentTag}
toggleCancel={this.onUpdateRepoTagToggle}
/>
}
</Fragment>
);
} else if ( !isFile && permission) {
return (

View File

@@ -0,0 +1,48 @@
.tag-list-container {
padding: 0.5rem;
padding-bottom: 0;
max-height: 15rem;
list-style: none;
overflow: auto;
}
.tag-list-item {
display: flex;
justify-content: space-around;
margin-bottom: 0.5rem;
}
.tag-list-item .tag-demo {
flex: 1;
border-radius: 0.25rem;
padding-left: 0.5rem;
display: flex;
align-items: center;
color: #ffffff;
}
.tag-list-item .tag-demo:hover {
border-left: 0.75rem solid #eb8205;
}
.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;
}
.tag-list-item .tag-edit:hover {
color: #092d42;
background-color: rgba(9,45,66,.13);
}
.tag-create .color-chooser,
.tag-edit .color-chooser {
margin-top: 0.5rem;
}

View File

@@ -0,0 +1,10 @@
class RepoTag {
constructor(object) {
this.id = object.repo_tag_id;
this.repo_id = object.repo_id;
this.name = object.tag_name;
this.color = object.tag_color;
}
}
export default RepoTag;

View File

@@ -76,6 +76,7 @@
.sf2-icon-two-columns:before { content:"\e036"; }
.sf2-icon-confirm:before {content:"\e01e"}
.sf2-icon-cancel:before {content:"\e01f"}
.sf2-icon-tag-manager:before {content:"\e015"}
/* common class and element style*/
a { color:#eb8205; }

View File

@@ -49,7 +49,7 @@ class RepoTagsView(APIView):
for tag in tag_list:
tags.append(tag.to_dict())
return Response({"tags": tags}, status=status.HTTP_200_OK)
return Response({"repo_tags": tags}, status=status.HTTP_200_OK)
def post(self, request, repo_id):
"""add one repo_tag.
@@ -87,7 +87,7 @@ class RepoTagsView(APIView):
error_msg = 'Internal Server Error.'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({"tag": repo_tag.to_dict()}, status=status.HTTP_201_CREATED)
return Response({"repo_tag": repo_tag.to_dict()}, status=status.HTTP_201_CREATED)
class RepoTagView(APIView):
@@ -95,15 +95,10 @@ class RepoTagView(APIView):
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def put(self, request, repo_id):
def put(self, request, repo_id, repo_tag_id):
"""update one repo_tag
"""
# argument check
tag_id = request.data.get('tag_id')
if not tag_id:
error_msg = 'tag_id invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
tag_name = request.data.get('name')
if not tag_name:
error_msg = 'name invalid.'
@@ -115,7 +110,7 @@ class RepoTagView(APIView):
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# resource check
repo_tag = RepoTags.objects.get_repo_tag_by_id(tag_id=tag_id)
repo_tag = RepoTags.objects.get_repo_tag_by_id(repo_tag_id)
if not repo_tag:
error_msg = 'repo_tag not found.'
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
@@ -134,19 +129,13 @@ class RepoTagView(APIView):
error_msg = 'Internal Server Error.'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({"tag": repo_tag.to_dict()}, status=status.HTTP_200_OK)
return Response({"repo_tag": repo_tag.to_dict()}, status=status.HTTP_200_OK)
def delete(self, request, repo_id):
def delete(self, request, repo_id, repo_tag_id):
"""delete one repo_tag
"""
# argument check
tag_id = request.data.get('tag_id')
if not tag_id:
error_msg = 'tag_id invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# resource check
repo_tag = RepoTags.objects.get_repo_tag_by_id(tag_id=tag_id)
repo_tag = RepoTags.objects.get_repo_tag_by_id(repo_tag_id)
if not repo_tag:
error_msg = 'repo_tag not found.'
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
@@ -157,7 +146,7 @@ class RepoTagView(APIView):
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
try:
RepoTags.objects.delete_repo_tag(tag_id)
RepoTags.objects.delete_repo_tag(repo_tag_id)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error.'

View File

@@ -14,9 +14,9 @@ class RepoTagsManager(models.Manager):
except self.model.DoesNotExist:
return None
def get_repo_tag_by_id(self, tag_id):
def get_repo_tag_by_id(self, repo_tag_id):
try:
return super(RepoTagsManager, self).get(pk=tag_id)
return super(RepoTagsManager, self).get(pk=repo_tag_id)
except self.model.DoesNotExist:
return None
@@ -28,9 +28,9 @@ class RepoTagsManager(models.Manager):
repo_tag.save()
return repo_tag
def delete_repo_tag(self, tag_id):
def delete_repo_tag(self, repo_tag_id):
try:
repo_tag = super(RepoTagsManager, self).get(pk=tag_id)
repo_tag = super(RepoTagsManager, self).get(pk=repo_tag_id)
repo_tag.delete()
return True
except self.model.DoesNotExist:
@@ -47,7 +47,7 @@ class RepoTags(models.Model):
def to_dict(self):
return {
"tag_id": self.pk,
"repo_tag_id": self.pk,
"repo_id": self.repo_id,
"tag_name": self.name,
"tag_color": self.color,

View File

@@ -294,7 +294,7 @@ urlpatterns = [
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/history/$', RepoHistory.as_view(), name='api-v2.1-repo-history'),
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/set-password/$', RepoSetPassword.as_view(), name="api-v2.1-repo-set-password"),
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/repo-tags/$', RepoTagsView.as_view(), name='api-v2.1-repo-tags'),
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/repo-tag/$', RepoTagView.as_view(), name='api-v2.1-repo-tag'),
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/repo-tags/(?P<repo_tag_id>\d+)/$', RepoTagView.as_view(), name='api-v2.1-repo-tag'),
## user::download-dir-zip-task
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/zip-task/$', ZipTaskView.as_view(), name='api-v2.1-zip-task'),