From d2b7db2caccb073359541f5342d1b186f636c434 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Tue, 16 Jul 2019 12:26:17 +0200
Subject: [PATCH] See #432: UI to suggest tags on tracks, albums and artists

---
 front/src/components/library/EditCard.vue     | 53 ++++++++-------
 front/src/components/library/EditForm.vue     | 22 +++++-
 front/src/components/library/TagsSelector.vue | 68 +++++++++++++++++++
 front/src/edits.js                            | 38 ++++++++++-
 front/src/lodash.js                           |  1 +
 5 files changed, 155 insertions(+), 27 deletions(-)
 create mode 100644 front/src/components/library/TagsSelector.vue

diff --git a/front/src/components/library/EditCard.vue b/front/src/components/library/EditCard.vue
index e72823aa7..bb8676cee 100644
--- a/front/src/components/library/EditCard.vue
+++ b/front/src/components/library/EditCard.vue
@@ -49,7 +49,7 @@
           </tr>
         </thead>
         <tbody>
-          <tr v-for="field in getUpdatedFields(obj.payload, previousState)" :key="field.id">
+          <tr v-for="field in updatedFields" :key="field.id">
             <td>{{ field.id }}</td>
 
             <td v-if="field.diff">
@@ -61,12 +61,12 @@
               <translate translate-context="*/*/*">N/A</translate>
             </td>
 
-            <td v-if="field.diff">
+            <td v-if="field.diff" :title="field.newRepr">
               <span v-if="!part.removed" v-for="part in field.diff" :class="['diff', {added: part.added}]">
                 {{ part.value }}
               </span>
             </td>
-            <td v-else>{{ field.new }}</td>
+            <td v-else :title="field.newRepr">{{ field.newRepr }}</td>
           </tr>
         </tbody>
       </table>
@@ -126,6 +126,7 @@ export default {
     }
   },
   computed: {
+    configs: edits.getConfigs,
     canApprove: edits.getCanApprove,
     canDelete: edits.getCanDelete,
     previousState () {
@@ -154,6 +155,32 @@ export default {
         namespace = 'library.artists.edit.detail'
       }
       return this.$router.resolve({name: namespace, params: {id, editId: this.obj.uuid}}).href
+    },
+
+    updatedFields () {
+      let payload = this.obj.payload
+      let previousState = this.previousState
+      let fields = Object.keys(payload)
+      let self = this
+      return fields.map((f) => {
+        let fieldConfig = edits.getFieldConfig(self.configs, this.obj.target.type, f)
+        let dummyRepr = (v) => { return v }
+        let getValueRepr = fieldConfig.getValueRepr || dummyRepr
+        let d = {
+          id: f,
+        }
+        if (previousState && previousState[f]) {
+          d.old = previousState[f]
+          d.oldRepr = castValue(getValueRepr(d.old.value))
+        }
+        d.new = payload[f]
+        d.newRepr = castValue(getValueRepr(d.new))
+        if (d.old) {
+          // we compute the diffs between the old and new values
+          d.diff = diffWordsWithSpace(d.oldRepr, d.newRepr)
+        }
+        return d
+      })
     }
   },
   methods: {
@@ -184,26 +211,6 @@ export default {
         self.isLoading = false
       })
     },
-    getUpdatedFields (payload, previousState) {
-      let fields = Object.keys(payload)
-      return fields.map((f) => {
-        let d = {
-          id: f,
-        }
-        if (previousState && previousState[f]) {
-          d.old = previousState[f]
-        }
-        d.new = payload[f]
-        if (d.old) {
-          // we compute the diffs between the old and new values
-
-          let oldValue = castValue(d.old.value)
-          let newValue = castValue(d.new)
-          d.diff = diffWordsWithSpace(oldValue, newValue)
-        }
-        return d
-      })
-    }
   }
 }
 </script>
diff --git a/front/src/components/library/EditForm.vue b/front/src/components/library/EditForm.vue
index 617917c68..ee462a1e2 100644
--- a/front/src/components/library/EditForm.vue
+++ b/front/src/components/library/EditForm.vue
@@ -77,10 +77,22 @@
           </button>
 
         </template>
+        <template v-else-if="fieldConfig.type === 'tags'">
+          <label :for="fieldConfig.id">{{ fieldConfig.label }}</label>
+          <tags-selector
+            ref="tags"
+            v-model="values[fieldConfig.id]"
+            :id="fieldConfig.id"
+            required="fieldConfig.required"></tags-selector>
+          <button class="ui tiny basic left floated button" form="noop" @click.prevent="values[fieldConfig.id] = []">
+            <i class="x icon"></i>
+            <translate translate-context="Content/Library/Button.Label">Clear</translate>
+          </button>
+        </template>
         <div v-if="values[fieldConfig.id] != initialValues[fieldConfig.id]">
           <button class="ui tiny basic right floated reset button" form="noop" @click.prevent="values[fieldConfig.id] = initialValues[fieldConfig.id]">
             <i class="undo icon"></i>
-            <translate translate-context="Content/Library/Button.Label" :translate-params="{value: initialValues[fieldConfig.id] || ''}">Reset to initial value: %{ value }</translate>
+            <translate translate-context="Content/Library/Button.Label">Reset to initial value</translate>
           </button>
         </div>
       </div>
@@ -110,13 +122,17 @@ import _ from '@/lodash'
 import axios from "axios"
 import EditList from '@/components/library/EditList'
 import EditCard from '@/components/library/EditCard'
+import TagsSelector from '@/components/library/TagsSelector'
 import edits from '@/edits'
 
+import lodash from '@/lodash'
+
 export default {
   props: ["objectType", "object", "licenses"],
   components: {
     EditList,
-    EditCard
+    EditCard,
+    TagsSelector
   },
   data() {
     return {
@@ -159,7 +175,7 @@ export default {
     mutationPayload () {
       let self = this
       let changedFields = this.config.fields.filter(f => {
-        return self.values[f.id] != self.initialValues[f.id]
+        return !lodash.isEqual(self.values[f.id], self.initialValues[f.id])
       })
       if (changedFields.length === 0) {
         return null
diff --git a/front/src/components/library/TagsSelector.vue b/front/src/components/library/TagsSelector.vue
new file mode 100644
index 000000000..5494fe9f3
--- /dev/null
+++ b/front/src/components/library/TagsSelector.vue
@@ -0,0 +1,68 @@
+<template>
+  <div ref="dropdown" class="ui multiple search selection dropdown">
+    <input type="hidden">
+    <i class="dropdown icon"></i>
+    <input type="text" class="search">
+    <div class="default text">
+      <translate translate-context="*/Dropdown/Placeholder/Verb">Search for existing tags…</translate>
+    </div>
+  </div>
+</template>
+<script>
+import $ from 'jquery'
+
+import lodash from '@/lodash'
+export default {
+  props: ['value'],
+  mounted () {
+    this.$nextTick(() => {
+      this.initDropdown()
+
+    })
+  },
+  methods: {
+    initDropdown () {
+      let self = this
+      let handleUpdate = () => {
+        let value = $(self.$refs.dropdown).dropdown('get value').split(',')
+        self.$emit('input', value)
+        return value
+      }
+      let settings = {
+        saveRemoteData: false,
+        filterRemoteData: true,
+        apiSettings: {
+          url: this.$store.getters['instance/absoluteUrl']('/api/v1/tags/?name__startswith={query}&ordering=length&page_size=5'),
+          beforeXHR: function (xhrObject) {
+            xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header'])
+            return xhrObject
+          },
+        },
+        fields: {
+          remoteValues: 'results',
+          value: 'name'
+        },
+        allowAdditions: true,
+        minCharacters: 1,
+        onAdd: handleUpdate,
+        onRemove: handleUpdate,
+        onLabelRemove: handleUpdate,
+        onChange: handleUpdate,
+      }
+      $(this.$refs.dropdown).dropdown(settings)
+      $(this.$refs.dropdown).dropdown('set exactly', this.value)
+    }
+  },
+  watch: {
+    value: {
+      handler (v) {
+        let current = $(this.$refs.dropdown).dropdown('get value').split(',').sort()
+        if (!lodash.isEqual([...v].sort(), current)) {
+          $(this.$refs.dropdown).dropdown('set exactly', v)
+        }
+      },
+      deep: true
+    }
+  }
+}
+</script>
diff --git a/front/src/edits.js b/front/src/edits.js
index 76e92e841..5c9e9be88 100644
--- a/front/src/edits.js
+++ b/front/src/edits.js
@@ -1,3 +1,10 @@
+function getTagsValueRepr (val) {
+  if (!val) {
+    return ''
+  }
+  return val.slice().sort().join('\n')
+}
+
 export default {
   getConfigs () {
     return {
@@ -10,6 +17,14 @@ export default {
             label: this.$pgettext('*/*/*/Noun', 'Name'),
             getValue: (obj) => { return obj.name }
           },
+          {
+            id: 'tags',
+            type: 'tags',
+            required: true,
+            label: this.$pgettext('*/*/*/Noun', 'Tags'),
+            getValue: (obj) => { return obj.tags },
+            getValueRepr: getTagsValueRepr
+          }
         ]
       },
       album: {
@@ -28,6 +43,14 @@ export default {
             label: this.$pgettext('Content/*/*/Noun', 'Release date'),
             getValue: (obj) => { return obj.release_date }
           },
+          {
+            id: 'tags',
+            type: 'tags',
+            required: true,
+            label: this.$pgettext('*/*/*/Noun', 'Tags'),
+            getValue: (obj) => { return obj.tags },
+            getValueRepr: getTagsValueRepr
+          }
         ]
       },
       track: {
@@ -61,6 +84,14 @@ export default {
             label: this.$pgettext('Content/*/*/Noun', 'License'),
             getValue: (obj) => { return obj.license },
           },
+          {
+            id: 'tags',
+            type: 'tags',
+            required: true,
+            label: this.$pgettext('*/*/*/Noun', 'Tags'),
+            getValue: (obj) => { return obj.tags },
+            getValueRepr: getTagsValueRepr
+          }
         ]
       }
     }
@@ -69,7 +100,12 @@ export default {
   getConfig () {
     return this.configs[this.objectType]
   },
-
+  getFieldConfig (configs, type, fieldId) {
+    let c = configs[type]
+    return c.fields.filter((f) => {
+      return f.id == fieldId
+    })[0]
+  },
   getCurrentState () {
     let self = this
     let s = {}
diff --git a/front/src/lodash.js b/front/src/lodash.js
index 8cd3ed92f..31fdbaa58 100644
--- a/front/src/lodash.js
+++ b/front/src/lodash.js
@@ -12,4 +12,5 @@ export default {
   uniq: require('lodash/uniq'),
   remove: require('lodash/remove'),
   reverse: require('lodash/reverse'),
+  isEqual: require('lodash/isEqual'),
 }
-- 
GitLab