Project tags persistence

pull/1297/head
Piero Toffanin 2023-03-07 13:24:26 -05:00
rodzic a7b09ee3fa
commit 26acc6ea1d
9 zmienionych plików z 117 dodań i 15 usunięć

Wyświetl plik

@ -10,6 +10,7 @@ from django.db.models import Q
from app import models
from .tasks import TaskIDsSerializer
from .tags import TagsField, parse_tags_input
from .common import get_and_check_project
from django.utils.translation import gettext as _
@ -23,6 +24,7 @@ class ProjectSerializer(serializers.ModelSerializer):
)
created_at = serializers.ReadOnlyField()
permissions = serializers.SerializerMethodField()
tags = TagsField(required=False)
def get_permissions(self, obj):
if 'request' in self.context:
@ -67,7 +69,7 @@ class ProjectViewSet(viewsets.ModelViewSet):
if self.paginator and self.request.query_params.get(self.paginator.page_query_param, None) is None:
return None
return super().paginate_queryset(queryset)
@action(detail=True, methods=['post'])
def duplicate(self, request, pk=None):
"""
@ -104,6 +106,7 @@ class ProjectViewSet(viewsets.ModelViewSet):
with transaction.atomic():
project.name = request.data.get('name', '')
project.description = request.data.get('description', '')
project.tags = TagsField().to_internal_value(parse_tags_input(request.data.get('tags', [])))
project.save()
form_perms = request.data.get('permissions')
@ -148,6 +151,7 @@ class ProjectViewSet(viewsets.ModelViewSet):
except User.DoesNotExist as e:
return Response({'error': _("Invalid user in permissions list")}, status=status.HTTP_400_BAD_REQUEST)
except AttributeError as e:
print(e)
return Response({'error': _("Invalid permissions")}, status=status.HTTP_400_BAD_REQUEST)
return Response({'success': True}, status=status.HTTP_200_OK)

Wyświetl plik

@ -1,8 +1,27 @@
from rest_framework import serializers
import json
class TagsField(serializers.JSONField):
def to_representation(self, tags):
return [t for t in tags.split(" ") if t != ""]
def to_internal_value(self, tags):
return " ".join([t.strip() for t in tags])
return " ".join([t.strip() for t in tags])
def parse_tags_input(tags):
if tags is None:
return []
if isinstance(tags, str):
try:
r = json.loads(tags)
if isinstance(r, list):
return r
else:
raise Exception("Invalid tags string")
except:
return []
elif isinstance(tags, list):
return list(map(str, tags))
else:
return []

Wyświetl plik

@ -42,7 +42,7 @@ class TaskSerializer(serializers.ModelSerializer):
processing_node_name = serializers.SerializerMethodField()
can_rerun_from = serializers.SerializerMethodField()
statistics = serializers.SerializerMethodField()
tags = TagsField()
tags = TagsField(required=False)
def get_processing_node_name(self, obj):
if obj.processing_node is not None:

Wyświetl plik

@ -28,10 +28,12 @@ class Dashboard extends React.Component {
return $.ajax({
url: `/api/projects/`,
type: 'POST',
data: {
contentType: 'application/json',
data: JSON.stringify({
name: project.name,
description: project.descr
}
description: project.descr,
tags: project.tags
})
}).done(() => {
this.projectList.refresh();
});

Wyświetl plik

@ -1,8 +1,10 @@
import React from 'react';
import '../css/EditProjectDialog.scss';
import FormDialog from './FormDialog';
import PropTypes from 'prop-types';
import ErrorMessage from './ErrorMessage';
import EditPermissionsPanel from './EditPermissionsPanel';
import TagsField from './TagsField';
import { _ } from '../classes/gettext';
class EditProjectDialog extends React.Component {
@ -10,6 +12,7 @@ class EditProjectDialog extends React.Component {
projectName: "",
projectDescr: "",
projectId: -1,
projectTags: [],
title: _("New Project"),
saveLabel: _("Create Project"),
savingLabel: _("Creating project..."),
@ -25,6 +28,7 @@ class EditProjectDialog extends React.Component {
projectName: PropTypes.string,
projectDescr: PropTypes.string,
projectId: PropTypes.number,
projectTags: PropTypes.array,
saveAction: PropTypes.func.isRequired,
onShow: PropTypes.func,
deleteAction: PropTypes.func,
@ -46,7 +50,9 @@ class EditProjectDialog extends React.Component {
name: props.projectName,
descr: props.projectDescr !== null ? props.projectDescr : "",
duplicating: false,
error: ""
tags: props.projectTags,
error: "",
showTagsField: !!props.projectTags.length
};
this.reset = this.reset.bind(this);
@ -60,6 +66,8 @@ class EditProjectDialog extends React.Component {
name: this.props.projectName,
descr: this.props.projectDescr,
duplicating: false,
tags: this.props.projectTags,
showTagsField: !!this.props.projectTags.length,
error: ""
});
}
@ -68,6 +76,7 @@ class EditProjectDialog extends React.Component {
const res = {
name: this.state.name,
descr: this.state.descr,
tags: this.state.tags
};
if (this.editPermissionsPanel){
@ -128,7 +137,26 @@ class EditProjectDialog extends React.Component {
});
}
toggleTagsField = () => {
if (!this.state.showTagsField){
setTimeout(() => {
if (this.tagsField) this.tagsField.focus();
}, 0);
}
this.setState({showTagsField: !this.state.showTagsField});
}
render(){
let tagsField = "";
if (this.state.showTagsField){
tagsField = (<div className="form-group">
<label className="col-sm-2 control-label">{_("Tags")}</label>
<div className="col-sm-10">
<TagsField onUpdate={(tags) => this.state.tags = tags } tags={this.state.tags} ref={domNode => this.tagsField = domNode}/>
</div>
</div>);
}
return (
<FormDialog {...this.props}
getFormData={this.getFormData}
@ -137,12 +165,16 @@ class EditProjectDialog extends React.Component {
leftButtons={this.props.showDuplicate ? [<button key="duplicate" disabled={this.duplicating} onClick={this.handleDuplicate} className="btn btn-default"><i className={"fa " + (this.state.duplicating ? "fa-circle-notch fa-spin fa-fw" : "fa-copy")}></i> Duplicate</button>] : undefined}
ref={(domNode) => { this.dialog = domNode; }}>
<ErrorMessage bind={[this, "error"]} />
<div className="form-group">
<div className="form-group edit-project-dialog">
<label className="col-sm-2 control-label">{_("Name")}</label>
<div className="col-sm-10">
<div className="col-sm-10 name-fields">
<input type="text" className="form-control" ref={(domNode) => { this.nameInput = domNode; }} value={this.state.name} onChange={this.handleChange('name')} onKeyPress={e => this.dialog.handleEnter(e)} />
<button type="button" title={_("Add tags")} onClick={this.toggleTagsField} className="btn btn-sm btn-secondary toggle-tags">
<i className="fa fa-tag"></i>
</button>
</div>
</div>
{tagsField}
<div className="form-group">
<label className="col-sm-2 control-label">{_("Description (optional)")}</label>
<div className="col-sm-10">

Wyświetl plik

@ -13,6 +13,7 @@ import csrf from '../django/csrf';
import HistoryNav from '../classes/HistoryNav';
import PropTypes from 'prop-types';
import ResizeModes from '../classes/ResizeModes';
import Tags from '../classes/Tags';
import exifr from '../vendor/exifr';
import { _, interpolate } from '../classes/gettext';
import $ from 'jquery';
@ -397,6 +398,7 @@ class ProjectListItem extends React.Component {
data: JSON.stringify({
name: project.name,
description: project.descr,
tags: project.tags,
permissions: project.permissions
}),
dataType: 'json',
@ -494,7 +496,7 @@ class ProjectListItem extends React.Component {
const { refreshing, data } = this.state;
const numTasks = data.tasks.length;
const canEdit = this.hasPermission("change");
const userTags = Tags.userTags(data.tags);
return (
<li className={"project-list-item list-group-item " + (refreshing ? "refreshing" : "")}
@ -514,6 +516,7 @@ class ProjectListItem extends React.Component {
projectName={data.name}
projectDescr={data.description}
projectId={data.id}
projectTags={data.tags}
saveAction={this.updateProject}
showPermissions={this.hasPermission("change")}
deleteAction={this.hasPermission("delete") ? this.handleDelete : undefined}
@ -556,6 +559,9 @@ class ProjectListItem extends React.Component {
<div className="project-name">
{data.name}
{userTags.length > 0 ?
userTags.map((t, i) => <div key={i} className="tag-badge small-badge">{t}</div>)
: ""}
</div>
<div className="project-description">
{data.description}

Wyświetl plik

@ -64,6 +64,8 @@ class TagsField extends React.Component {
e.preventDefault();
e.stopPropagation();
this.addTag();
}else if (e.key === "Backspace" && this.inputText.innerText === ""){
this.removeTag(this.state.userTags.length - 1);
}
}
@ -75,19 +77,26 @@ class TagsField extends React.Component {
e.stopPropagation();
}
removeTag = idx => {
handleRemoveTag = idx => {
return e => {
e.stopPropagation();
this.setState(update(this.state, { userTags: { $splice: [[idx, 1]] } }));
this.removeTag(idx);
}
}
removeTag = idx => {
this.setState(update(this.state, { userTags: { $splice: [[idx, 1]] } }));
}
addTag = () => {
const text = this.inputText.innerText;
let text = this.inputText.innerText;
if (text !== ""){
// Do not allow system tags
if (!text.startsWith("_")){
// Only lower case text allowed
text = text.toLowerCase();
// Check for dulicates
if (this.state.userTags.indexOf(text) === -1){
this.setState(update(this.state, {
@ -204,7 +213,7 @@ class TagsField extends React.Component {
<div draggable="true" className="tag-badge" key={i} ref={domNode => this.domTags[i] = domNode}
onClick={this.stop}
onDragStart={this.handleDragStart(tag)}
onDragEnd={this.handleDragEnd}>{tag} <a href="javascript:void(0)" onClick={this.removeTag(i)}>×</a>&nbsp;&nbsp;</div>
onDragEnd={this.handleDragEnd}>{tag} <a href="javascript:void(0)" onClick={this.handleRemoveTag(i)}>×</a>&nbsp;&nbsp;</div>
)}
<div className="inputText" contentEditable="true" ref={(domNode) => this.inputText = domNode}
onKeyDown={this.handleKeyDown}

Wyświetl plik

@ -0,0 +1,15 @@
.edit-project-dialog{
.name-fields{
display: flex;
.btn.toggle-tags{
margin-top: 0;
margin-bottom: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
input[type="text"]{
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
}

Wyświetl plik

@ -105,4 +105,19 @@
.task-filters{
float: right;
}
.tag-badge.small-badge {
display: inline-block;
width: auto;
padding-left: 6px;
padding-right: 6px;
padding-top: 0px;
padding-bottom: 0px;
margin-left: 4px;
margin-top: -2px;
border-radius: 6px;
font-size: 90%;
position: relative;
top: -1px;
}
}