From b6fc2b7d105ff2663aebb55b2643142897840ed0 Mon Sep 17 00:00:00 2001
From: "J.-S. Caux" <J.S.Caux@uva.nl>
Date: Sat, 8 Feb 2020 14:25:07 +0100
Subject: [PATCH] Add TagListEditable

---
 apimail/api/serializers.py                    |   2 +-
 apimail/api/views.py                          |  23 +++
 .../assets/vue/components/MessagesTable.vue   |  62 +++++--
 .../assets/vue/components/TagListEditable.vue | 154 ++++++++++++++++++
 apimail/urls.py                               |  10 ++
 5 files changed, 235 insertions(+), 16 deletions(-)
 create mode 100644 apimail/static/apimail/assets/vue/components/TagListEditable.vue

diff --git a/apimail/api/serializers.py b/apimail/api/serializers.py
index 7961710d1..691898784 100644
--- a/apimail/api/serializers.py
+++ b/apimail/api/serializers.py
@@ -81,7 +81,7 @@ class EventSerializer(serializers.ModelSerializer):
 class UserTagSerializer(serializers.ModelSerializer):
     class Meta:
         model = UserTag
-        fields = ['pk', 'label', 'unicode_symbol', 'variant']
+        fields = ['pk', 'user', 'label', 'unicode_symbol', 'variant']
 
     def get_queryset(self):
         user = self.request.user
diff --git a/apimail/api/views.py b/apimail/api/views.py
index 04855345d..0f6c4f144 100644
--- a/apimail/api/views.py
+++ b/apimail/api/views.py
@@ -229,6 +229,29 @@ class StoredMessageUpdateReadAPIView(UpdateAPIView):
         return Response()
 
 
+class UserTagCreateAPIView(CreateAPIView):
+    permission_classes = (IsAuthenticated,)
+    queryset = UserTag.objects.all()
+    serializer_class = UserTagSerializer
+
+    def create(self, request, *args, **kwargs):
+        data = request.data
+        data['user'] = request.user.id
+        serializer = self.get_serializer(data=data)
+        serializer.is_valid(raise_exception=True)
+        self.perform_create(serializer)
+        headers = self.get_success_headers(serializer.data)
+        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
+
+
+class UserTagDestroyAPIView(DestroyAPIView):
+    permission_classes = (IsAuthenticated,)
+    serializer_class = UserTagSerializer
+
+    def get_queryset(self):
+        return UserTag.objects.filter(user=self.request.user)
+
+
 class UserTagListAPIView(ListAPIView):
     permission_classes = (IsAuthenticated,)
     serializer_class = UserTagSerializer
diff --git a/apimail/static/apimail/assets/vue/components/MessagesTable.vue b/apimail/static/apimail/assets/vue/components/MessagesTable.vue
index e8937341c..242d85312 100644
--- a/apimail/static/apimail/assets/vue/components/MessagesTable.vue
+++ b/apimail/static/apimail/assets/vue/components/MessagesTable.vue
@@ -17,6 +17,19 @@
     </template>
   </b-modal>
 
+  <b-modal
+    id="modal-manage-tags"
+    title="Manage your Tags"
+    hide-header-close
+    >
+    <tag-list-editable :tags="tags" @fetchtags="fetchTags"></tag-list-editable>
+    <template v-slot:modal-footer="{ close, }">
+      <b-button size="sm" variant="danger" @click="close()">
+	Done
+      </b-button>
+    </template>
+  </b-modal>
+
 
   <div v-if="draftMessages.length > 0" class="m-2 mb-4">
     <h2>Message drafts to complete</h2>
@@ -130,7 +143,7 @@
 	    </b-form-radio-group>
 	  </b-form-group>
 	</b-col>
-	<b-col class="col-lg-6">
+	<b-col class="col-lg-5">
 	  <b-form-group
 	    label="Tag:"
 	    label-cols-sm="3"
@@ -147,6 +160,14 @@
 	    </b-form-radio-group>
 	  </b-form-group>
 	</b-col>
+	<b-col class="col-lg-1">
+	  <b-button
+	    size="sm"
+	    @click="showManageTagsModal"
+	    >
+	    <small>Manage your tags</small>
+	  </b-button>
+	</b-col>
       </b-row>
       <hr>
       <b-row class="mb-0">
@@ -273,6 +294,8 @@ import MessageContent from './MessageContent.vue'
 
 import MessageComposer from './MessageComposer.vue'
 
+import TagListEditable from './TagListEditable.vue'
+
 var csrftoken = Cookies.get('csrftoken');
 
 export default {
@@ -280,6 +303,7 @@ export default {
     components: {
 	MessageContent,
 	MessageComposer,
+	TagListEditable,
     },
     data() {
 	return {
@@ -317,6 +341,7 @@ export default {
 		{ text: 'read', value: true },
 		{ text: 'all', value: null },
 	    ],
+	    tags: null,
 	    tagRequired: 'any',
 	}
     },
@@ -339,25 +364,30 @@ export default {
 		.then(data => this.draftMessages = data.results)
 		.catch(error => console.error(error))
 	},
+	showManageTagsModal () {
+	    this.$bvModal.show('modal-manage-tags')
+	},
 	showReworkDraftModal (draftmsg) {
 	    this.draftMessageSelected = draftmsg
 	    this.$bvModal.show('modal-resumedraft')
 	},
 	deleteDraft (uuid) {
-	    fetch('/mail/api/composed_message/' + uuid + '/delete',
-		  {
-		      method: 'DELETE',
-		      headers: {
-			  "X-CSRFToken": csrftoken,
+	    if (confirm("Are you sure you want to delete this draft?")) {
+		fetch('/mail/api/composed_message/' + uuid + '/delete',
+		      {
+			  method: 'DELETE',
+			  headers: {
+			      "X-CSRFToken": csrftoken,
+			  }
 		      }
-		  }
-		 )
-		.then(response => {
-		    if (response.ok) {
-			this.fetchDrafts()
-		    }
-		})
-		.catch(error => console.error(error))
+		     )
+		    .then(response => {
+			if (response.ok) {
+			    this.fetchDrafts()
+			}
+		    })
+		    .catch(error => console.error(error))
+	    }
 	},
 	isSelected: function (selection) {
 	    return selection === this.accountSelected
@@ -417,7 +447,9 @@ export default {
 	this.fetchTags()
 	this.fetchDrafts()
 	this.$root.$on('bv::modal::hide', (bvEvent, modalId) => {
-	    this.fetchDrafts()
+	    if (bvEvent.componentId === 'modal-resumedraft') {
+		this.fetchDrafts()
+	    }
 	})
     },
     watch: {
diff --git a/apimail/static/apimail/assets/vue/components/TagListEditable.vue b/apimail/static/apimail/assets/vue/components/TagListEditable.vue
new file mode 100644
index 000000000..8149bfba2
--- /dev/null
+++ b/apimail/static/apimail/assets/vue/components/TagListEditable.vue
@@ -0,0 +1,154 @@
+<template>
+<div>
+  <h3>Your current tags:</h3>
+  <table class="table">
+    <tr v-for="tag in tags" class="mb-4">
+      <td>
+	<b-button
+	  size="sm"
+	  class="p-1"
+	  :variant="tag.variant"
+	  >
+	  {{ tag.unicode_symbol }}
+	</b-button>
+      </td>
+      <td>{{ tag.label }}</td>
+      <td>
+	<b-button class="float-right bg-danger text-white px-1 py-0" @click.stop="deleteTag(tag.pk)">
+	  <small>Delete</small>
+	</b-button>
+      </td>
+    </tr>
+  </table>
+  <h3>Create a new tag:</h3>
+  <b-form
+    >
+    <b-form-group
+      id="label"
+      label="Label:"
+      label-for="input-label"
+      >
+      <b-form-input
+	id="input-label"
+	v-model="newTagForm.label"
+	required
+	placeholder="Enter a label for your new Tag"
+	>
+      </b-form-input>
+    </b-form-group>
+    <b-form-group
+      id="unicode_symbol"
+      label="Unicode symbol:"
+      label-for="input-unicode-symbol"
+      >
+      <b-form-input
+	id="input-unicode-symbol"
+	v-model="newTagForm.unicode_symbol"
+	required
+	placeholder="Enter a single (arbitrary) unicode character"
+	>
+      </b-form-input>
+    </b-form-group>
+    <b-form-group
+      id="variant"
+      label="Variant:"
+      label-for="input-variant"
+      >
+      <b-form-select
+	id="input-variant"
+	:options="variantOptions"
+	v-model="newTagForm.variant"
+	>
+      </b-form-select>
+    </b-form-group>
+  </b-form>
+  <b-button
+    variant="success"
+    class="text-white"
+    @click.stop.prevent="createNewTag"
+    >
+    Create new Tag
+  </b-button>
+</div>
+</template>
+
+<script>
+import Cookies from 'js-cookie'
+
+var csrftoken = Cookies.get('csrftoken');
+
+export default {
+    props: {
+	tags: {
+	    type: Array,
+	    required: false,
+	},
+    },
+    data() {
+	return {
+	    newTagForm: {
+		label: null,
+		unicode_symbol: null,
+		variant: null
+	    },
+	    variantOptions: [
+		{ text: 'primary', value: 'primary' },
+		{ text: 'secondary', value: 'secondary' },
+		{ text: 'success', value: 'success' },
+		{ text: 'warning', value: 'warning' },
+		{ text: 'danger', value: 'danger' },
+		{ text: 'info', value: 'info' },
+		{ text: 'light', value: 'light' },
+		{ text: 'dark', value: 'dark' },
+	    ]
+	}
+    },
+    methods: {
+	createNewTag () {
+	    fetch('/mail/api/user_tag/create',
+		  {
+		      method: 'POST',
+		      headers: {
+			  "X-CSRFToken": csrftoken,
+	    		  "Content-Type": "application/json; charset=utf-8"
+		      },
+		      body: JSON.stringify({
+			  'label': this.newTagForm.label,
+			  'unicode_symbol': this.newTagForm.unicode_symbol,
+			  'variant': this.newTagForm.variant,
+		      })
+		  })
+		.then(response => {
+		    if (response.ok) {
+			this.newTagForm.label = null,
+			this.newTagForm.unicode_symbol = null,
+			this.newTagForm.variant = null
+			this.$emit('fetchtags')
+		    }
+		    else {
+			console.log(response.data)
+		    }
+		})
+		.catch(error => console.error(error))
+	},
+	deleteTag (pk) {
+	    if (confirm("Do you really want to delete this tag? " +
+			"It will be immediately removed from all messages.")) {
+		fetch('/mail/api/user_tag/' + pk + '/delete',
+		      {
+			  method: 'DELETE',
+			  headers: {
+			      "X-CSRFToken": csrftoken,
+			  }
+		      })
+		    .then(response => {
+			if (response.ok) {
+			    this.$emit('fetchtags')
+			}
+		    })
+		    .catch(error => console.error(error))
+	    }
+	}
+    }
+}
+</script>
diff --git a/apimail/urls.py b/apimail/urls.py
index c87ba7294..eb15453af 100644
--- a/apimail/urls.py
+++ b/apimail/urls.py
@@ -82,6 +82,16 @@ urlpatterns = [
             apiviews.StoredMessageUpdateTagAPIView.as_view(),
             name='api_stored_message_tag'
         ),
+        path( # /mail/api/user_tag/create
+            'user_tag/create',
+            apiviews.UserTagCreateAPIView.as_view(),
+            name='user_tag_create'
+        ),
+        path( # /mail/api/user_tag/<pk>/delete
+            'user_tag/<int:pk>/delete',
+            apiviews.UserTagDestroyAPIView.as_view(),
+            name='user_tag_delete'
+        ),
         path( # /mail/api/user_tags
             'user_tags',
             apiviews.UserTagListAPIView.as_view(),
-- 
GitLab