See #432: UI to suggest tags on tracks, albums and artists
This commit is contained in:
parent
9336fec430
commit
d2b7db2cac
|
@ -49,7 +49,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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>{{ field.id }}</td>
|
||||||
|
|
||||||
<td v-if="field.diff">
|
<td v-if="field.diff">
|
||||||
|
@ -61,12 +61,12 @@
|
||||||
<translate translate-context="*/*/*">N/A</translate>
|
<translate translate-context="*/*/*">N/A</translate>
|
||||||
</td>
|
</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}]">
|
<span v-if="!part.removed" v-for="part in field.diff" :class="['diff', {added: part.added}]">
|
||||||
{{ part.value }}
|
{{ part.value }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td v-else>{{ field.new }}</td>
|
<td v-else :title="field.newRepr">{{ field.newRepr }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -126,6 +126,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
configs: edits.getConfigs,
|
||||||
canApprove: edits.getCanApprove,
|
canApprove: edits.getCanApprove,
|
||||||
canDelete: edits.getCanDelete,
|
canDelete: edits.getCanDelete,
|
||||||
previousState () {
|
previousState () {
|
||||||
|
@ -154,6 +155,32 @@ export default {
|
||||||
namespace = 'library.artists.edit.detail'
|
namespace = 'library.artists.edit.detail'
|
||||||
}
|
}
|
||||||
return this.$router.resolve({name: namespace, params: {id, editId: this.obj.uuid}}).href
|
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: {
|
methods: {
|
||||||
|
@ -184,26 +211,6 @@ export default {
|
||||||
self.isLoading = false
|
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>
|
</script>
|
||||||
|
|
|
@ -77,10 +77,22 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</template>
|
</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]">
|
<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]">
|
<button class="ui tiny basic right floated reset button" form="noop" @click.prevent="values[fieldConfig.id] = initialValues[fieldConfig.id]">
|
||||||
<i class="undo icon"></i>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -110,13 +122,17 @@ import _ from '@/lodash'
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import EditList from '@/components/library/EditList'
|
import EditList from '@/components/library/EditList'
|
||||||
import EditCard from '@/components/library/EditCard'
|
import EditCard from '@/components/library/EditCard'
|
||||||
|
import TagsSelector from '@/components/library/TagsSelector'
|
||||||
import edits from '@/edits'
|
import edits from '@/edits'
|
||||||
|
|
||||||
|
import lodash from '@/lodash'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ["objectType", "object", "licenses"],
|
props: ["objectType", "object", "licenses"],
|
||||||
components: {
|
components: {
|
||||||
EditList,
|
EditList,
|
||||||
EditCard
|
EditCard,
|
||||||
|
TagsSelector
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -159,7 +175,7 @@ export default {
|
||||||
mutationPayload () {
|
mutationPayload () {
|
||||||
let self = this
|
let self = this
|
||||||
let changedFields = this.config.fields.filter(f => {
|
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) {
|
if (changedFields.length === 0) {
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -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>
|
|
@ -1,3 +1,10 @@
|
||||||
|
function getTagsValueRepr (val) {
|
||||||
|
if (!val) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return val.slice().sort().join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getConfigs () {
|
getConfigs () {
|
||||||
return {
|
return {
|
||||||
|
@ -10,6 +17,14 @@ export default {
|
||||||
label: this.$pgettext('*/*/*/Noun', 'Name'),
|
label: this.$pgettext('*/*/*/Noun', 'Name'),
|
||||||
getValue: (obj) => { return obj.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: {
|
album: {
|
||||||
|
@ -28,6 +43,14 @@ export default {
|
||||||
label: this.$pgettext('Content/*/*/Noun', 'Release date'),
|
label: this.$pgettext('Content/*/*/Noun', 'Release date'),
|
||||||
getValue: (obj) => { return obj.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: {
|
track: {
|
||||||
|
@ -61,6 +84,14 @@ export default {
|
||||||
label: this.$pgettext('Content/*/*/Noun', 'License'),
|
label: this.$pgettext('Content/*/*/Noun', 'License'),
|
||||||
getValue: (obj) => { return obj.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 () {
|
getConfig () {
|
||||||
return this.configs[this.objectType]
|
return this.configs[this.objectType]
|
||||||
},
|
},
|
||||||
|
getFieldConfig (configs, type, fieldId) {
|
||||||
|
let c = configs[type]
|
||||||
|
return c.fields.filter((f) => {
|
||||||
|
return f.id == fieldId
|
||||||
|
})[0]
|
||||||
|
},
|
||||||
getCurrentState () {
|
getCurrentState () {
|
||||||
let self = this
|
let self = this
|
||||||
let s = {}
|
let s = {}
|
||||||
|
|
|
@ -12,4 +12,5 @@ export default {
|
||||||
uniq: require('lodash/uniq'),
|
uniq: require('lodash/uniq'),
|
||||||
remove: require('lodash/remove'),
|
remove: require('lodash/remove'),
|
||||||
reverse: require('lodash/reverse'),
|
reverse: require('lodash/reverse'),
|
||||||
|
isEqual: require('lodash/isEqual'),
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue