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