238 lines
8.6 KiB
Vue
238 lines
8.6 KiB
Vue
<template>
|
|
<div class="ui fluid card">
|
|
<div class="content">
|
|
<h4 class="header">
|
|
<router-link :to="detailUrl">
|
|
<translate translate-context="Content/Library/Card/Short" :translate-params="{id: obj.uuid.substring(0, 8)}">Modification %{ id }</translate>
|
|
</router-link>
|
|
</h4>
|
|
<div class="meta">
|
|
<router-link
|
|
v-if="obj.target && obj.target.type === 'track'"
|
|
:to="{name: 'library.tracks.detail', params: {id: obj.target.id }}">
|
|
<i class="music icon"></i>
|
|
<translate translate-context="Content/Library/Card/Short" :translate-params="{id: obj.target.id, name: obj.target.repr}">Track #%{ id } - %{ name }</translate>
|
|
</router-link>
|
|
<br>
|
|
<human-date :date="obj.creation_date" :icon="true"></human-date>
|
|
|
|
<span class="right floated">
|
|
<span v-if="obj.is_approved && obj.is_applied">
|
|
<i class="success check icon"></i>
|
|
<translate translate-context="Content/Library/Card/Short">Approved and applied</translate>
|
|
</span>
|
|
<span v-else-if="obj.is_approved">
|
|
<i class="success check icon"></i>
|
|
<translate translate-context="Content/*/*/Short">Approved</translate>
|
|
</span>
|
|
<span v-else-if="obj.is_approved === null">
|
|
<i class="warning hourglass icon"></i>
|
|
<translate translate-context="Content/Admin/*/Noun">Pending review</translate>
|
|
</span>
|
|
<span v-else-if="obj.is_approved === false">
|
|
<i class="danger x icon"></i>
|
|
<translate translate-context="Content/Library/*/Short">Rejected</translate>
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div v-if="obj.summary" class="content">
|
|
{{ obj.summary }}
|
|
</div>
|
|
<div class="content">
|
|
<table v-if="obj.type === 'update'" class="ui celled very basic fixed stacking table">
|
|
<thead>
|
|
<tr>
|
|
<th><translate translate-context="Content/Library/Card.Table.Header/Short">Field</translate></th>
|
|
<th><translate translate-context="Content/Library/Card.Table.Header/Short">Old value</translate></th>
|
|
<th><translate translate-context="Content/Library/Card.Table.Header/Short">New value</translate></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="field in updatedFields" :key="field.id">
|
|
<td>{{ field.id }}</td>
|
|
|
|
<td v-if="field.diff">
|
|
<template v-if="field.config.type === 'attachment' && field.oldRepr">
|
|
<img class="ui image" :src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.oldRepr}/proxy?next=medium_square_crop`)" />
|
|
</template>
|
|
<template v-else>
|
|
<span v-if="!part.added" v-for="part in field.diff" :class="['diff', {removed: part.removed}]">
|
|
{{ part.value }}
|
|
</span>
|
|
</template>
|
|
</td>
|
|
<td v-else>
|
|
<translate translate-context="*/*/*">N/A</translate>
|
|
</td>
|
|
|
|
<td v-if="field.diff" :title="field.newRepr">
|
|
<template v-if="field.config.type === 'attachment' && field.newRepr">
|
|
<img class="ui image" :src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)" />
|
|
</template>
|
|
<template v-else>
|
|
<span v-if="!part.removed" v-for="part in field.diff" :class="['diff', {added: part.added}]">
|
|
{{ part.value }}
|
|
</span>
|
|
</template>
|
|
</td>
|
|
<td v-else :title="field.newRepr">
|
|
<template v-if="field.config.type === 'attachment' && field.newRepr">
|
|
<img class="ui image" :src="$store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)" />
|
|
</template>
|
|
<template v-else>
|
|
{{ field.newRepr }}
|
|
</template>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-if="obj.created_by" class="extra content">
|
|
<actor-link :actor="obj.created_by" />
|
|
</div>
|
|
<div v-if="canDelete || canApprove" class="ui bottom attached buttons">
|
|
<button
|
|
v-if="canApprove && obj.is_approved !== true"
|
|
@click="approve(true)"
|
|
:class="['ui', {loading: isLoading}, 'success', 'basic', 'button']">
|
|
<translate translate-context="Content/*/Button.Label/Verb">Approve</translate>
|
|
</button>
|
|
<button
|
|
v-if="canApprove && obj.is_approved === null"
|
|
@click="approve(false)"
|
|
:class="['ui', {loading: isLoading}, 'warning', 'basic', 'button']">
|
|
<translate translate-context="Content/Library/Button.Label">Reject</translate>
|
|
</button>
|
|
<dangerous-button
|
|
v-if="canDelete"
|
|
:class="['ui', {loading: isLoading}, 'basic danger button']"
|
|
:action="remove">
|
|
<translate translate-context="*/*/*/Verb">Delete</translate>
|
|
<p slot="modal-header"><translate translate-context="Popup/Library/Title">Delete this suggestion?</translate></p>
|
|
<div slot="modal-content">
|
|
<p><translate translate-context="Popup/Library/Paragraph">The suggestion will be completely removed, this action is irreversible.</translate></p>
|
|
</div>
|
|
<p slot="modal-confirm"><translate translate-context="*/*/*/Verb">Delete</translate></p>
|
|
</dangerous-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import axios from 'axios'
|
|
import { diffWordsWithSpace } from 'diff'
|
|
|
|
import edits from '@/edits'
|
|
|
|
function castValue (value) {
|
|
if (value === null || value === undefined) {
|
|
return ''
|
|
}
|
|
return String(value)
|
|
}
|
|
|
|
export default {
|
|
props: {
|
|
obj: {required: true},
|
|
currentState: {required: false}
|
|
},
|
|
data () {
|
|
return {
|
|
isLoading: false
|
|
}
|
|
},
|
|
computed: {
|
|
configs: edits.getConfigs,
|
|
canApprove: edits.getCanApprove,
|
|
canDelete: edits.getCanDelete,
|
|
previousState () {
|
|
if (this.obj.is_applied) {
|
|
// mutation was applied, we use the previous state that is stored
|
|
// on the mutation itself
|
|
return this.obj.previous_state
|
|
}
|
|
// mutation is not applied yet, so we use the current state that was
|
|
// passed to the component, if any
|
|
return this.currentState
|
|
},
|
|
detailUrl () {
|
|
if (!this.obj.target) {
|
|
return ''
|
|
}
|
|
let namespace
|
|
let id = this.obj.target.id
|
|
if (this.obj.target.type === 'track') {
|
|
namespace = 'library.tracks.edit.detail'
|
|
}
|
|
if (this.obj.target.type === 'album') {
|
|
namespace = 'library.albums.edit.detail'
|
|
}
|
|
if (this.obj.target.type === 'artist') {
|
|
namespace = 'library.artists.edit.detail'
|
|
}
|
|
return this.$router.resolve({name: namespace, params: {id, editId: this.obj.uuid}}).href
|
|
},
|
|
|
|
updatedFields () {
|
|
if (!this.obj.target) {
|
|
return []
|
|
}
|
|
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,
|
|
config: fieldConfig
|
|
}
|
|
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: {
|
|
remove () {
|
|
let self = this
|
|
this.isLoading = true
|
|
axios.delete(`mutations/${this.obj.uuid}/`).then((response) => {
|
|
self.$emit('deleted')
|
|
self.isLoading = false
|
|
}, error => {
|
|
self.isLoading = false
|
|
})
|
|
},
|
|
approve (approved) {
|
|
let url
|
|
if (approved) {
|
|
url = `mutations/${this.obj.uuid}/approve/`
|
|
} else {
|
|
url = `mutations/${this.obj.uuid}/reject/`
|
|
}
|
|
let self = this
|
|
this.isLoading = true
|
|
axios.post(url).then((response) => {
|
|
self.$emit('approved', approved)
|
|
self.isLoading = false
|
|
self.$store.commit('ui/incrementNotifications', {count: -1, type: 'pendingReviewEdits'})
|
|
}, error => {
|
|
self.isLoading = false
|
|
})
|
|
},
|
|
}
|
|
}
|
|
</script>
|