Commit 6586b2b7 authored by Eliot Berriot's avatar Eliot Berriot 💬

See #228: smarter action table with shift-click select

parent eded32c2
<template>
<table class="ui compact very basic single line unstackable table">
<thead>
<tr v-if="actions.length > 0 && objectsData.count > 0">
<tr v-if="actions.length > 0">
<th colspan="1000">
<div class="ui small form">
<div class="ui inline fields">
<div class="field">
<label>{{ $t('Actions') }}</label>
<select class="ui dropdown" v-model="currentAction">
<option v-for="action in actions" :value="action[0]">
{{ action[1] }}
<select class="ui dropdown" v-model="currentActionName">
<option v-for="action in actions" :value="action.name">
{{ action.label }}
</option>
</select>
</div>
<div class="field">
<div
v-if="!selectAll"
@click="launchAction"
:disabled="checked.length === 0"
:class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']">
{{ $t('Go') }}</div>
<dangerous-button
v-else :class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']"
confirm-color="green"
color=""
@confirm="launchAction">
{{ $t('Go') }}
<p slot="modal-header">{{ $t('Do you want to launch action "{% action %}" on {% total %} elements?', {action: currentActionName, total: objectsData.count}) }}
<p slot="modal-content">
{{ $t('This may affect a lot of elements, please double check this is really what you want.')}}
</p>
<p slot="modal-confirm">{{ $t('Launch') }}</p>
</dangerous-button>
</div>
<div class="count field">
<span v-if="selectAll">{{ $t('{% count %} on {% total %} selected', {count: objectsData.count, total: objectsData.count}) }}</span>
<span v-else>{{ $t('{% count %} on {% total %} selected', {count: checked.length, total: objectsData.count}) }}</span>
<template v-if="checked.length === objectsData.results.length">
<template v-if="checkable.length === checked.length">
<a @click="selectAll = true" v-if="!selectAll">
{{ $t('Select all {% total %} elements', {total: objectsData.count}) }}
</a>
......@@ -53,18 +66,20 @@
<input
type="checkbox"
@change="toggleCheckAll"
:checked="objectsData.results.length === checked.length"><label>&nbsp;</label>
:disabled="checkable.length === 0"
:checked="checkable.length > 0 && checked.length === checkable.length"><label>&nbsp;</label>
</div>
</th>
<slot name="header-cells"></slot>
</tr>
</thead>
<tbody>
<tr v-for="obj in objectsData.results">
<tbody v-if="objectsData.count > 0">
<tr v-for="(obj, index) in objectsData.results">
<td class="collapsing">
<input
type="checkbox"
@change="toggleCheck(obj.id)"
:disabled="checkable.indexOf(obj.id) === -1"
@click="toggleCheck($event, obj.id, index)"
:checked="checked.indexOf(obj.id) > -1"><label>&nbsp;</label>
</div>
</td>
......@@ -90,38 +105,60 @@ export default {
actionLoading: false,
actionResult: null,
actionErrors: [],
currentAction: null,
selectAll: false
currentActionName: null,
selectAll: false,
lastCheckedIndex: -1
}
if (this.actions.length > 0) {
d.currentAction = this.actions[0][0]
d.currentActionName = this.actions[0].name
}
return d
},
methods: {
toggleCheckAll () {
if (this.checked.length === this.objectsData.results.length) {
this.lastCheckedIndex = -1
if (this.checked.length === this.checkable.length) {
// we uncheck
this.checked = []
} else {
this.checked = this.objectsData.results.map(t => { return t.id })
this.checked = this.checkable.map(i => { return i })
}
},
toggleCheck (id) {
toggleCheck (event, id, index) {
let self = this
let affectedIds = [id]
let newValue = null
if (this.checked.indexOf(id) > -1) {
// we uncheck
this.selectAll = false
this.checked.splice(this.checked.indexOf(id), 1)
newValue = false
} else {
this.checked.push(id)
newValue = true
}
if (event.shiftKey && this.lastCheckedIndex > -1) {
// we also add inbetween ids to the list of affected ids
let idxs = [index, this.lastCheckedIndex]
idxs.sort((a, b) => a - b)
let objs = this.objectsData.results.slice(idxs[0], idxs[1] + 1)
affectedIds = affectedIds.concat(objs.map((o) => { return o.id }))
}
affectedIds.forEach((i) => {
let checked = self.checked.indexOf(i) > -1
if (newValue && !checked && self.checkable.indexOf(i) > -1) {
return self.checked.push(i)
}
if (!newValue && checked) {
self.checked.splice(self.checked.indexOf(i), 1)
}
})
this.lastCheckedIndex = index
},
launchAction () {
let self = this
self.actionLoading = true
self.result = null
let payload = {
action: this.currentAction,
action: this.currentActionName,
filters: this.filters
}
if (this.selectAll) {
......@@ -132,11 +169,39 @@ export default {
axios.post(this.actionUrl, payload).then((response) => {
self.actionResult = response.data
self.actionLoading = false
self.$emit('action-launched', response.data)
}, error => {
self.actionLoading = false
self.actionErrors = error.backendErrors
})
}
},
computed: {
currentAction () {
let self = this
return this.actions.filter((a) => {
return a.name === self.currentActionName
})[0]
},
checkable () {
let objs = this.objectsData.results
let filter = this.currentAction.filterCheckable
if (filter) {
objs = objs.filter((o) => {
return filter(o)
})
}
return objs.map((o) => { return o.id })
}
},
watch: {
objectsData: {
handler () {
this.checked = []
this.selectAll = false
},
deep: true
}
}
}
</script>
......
......@@ -13,7 +13,7 @@
</div>
<div class="actions">
<div class="ui cancel button"><i18next path="Cancel"/></div>
<div :class="['ui', 'confirm', color, 'button']" @click="confirm">
<div :class="['ui', 'confirm', confirmButtonColor, 'button']" @click="confirm">
<slot name="modal-confirm"><i18next path="Confirm"/></slot>
</div>
</div>
......@@ -28,7 +28,8 @@ export default {
props: {
action: {type: Function, required: false},
disabled: {type: Boolean, default: false},
color: {type: String, default: 'red'}
color: {type: String, default: 'red'},
confirmColor: {type: String, default: null, required: false}
},
components: {
Modal
......@@ -38,6 +39,14 @@ export default {
showModal: false
}
},
computed: {
confirmButtonColor () {
if (this.confirmColor) {
return this.confirmColor
}
return this.color
}
},
methods: {
confirm () {
this.showModal = false
......
......@@ -10,55 +10,63 @@
<label>{{ $t('Import status') }}</label>
<select class="ui dropdown" v-model="importedFilter">
<option :value="null">{{ $t('Any') }}</option>
<option :value="true">{{ $t('Imported') }}</option>
<option :value="false">{{ $t('Not imported') }}</option>
<option :value="'imported'">{{ $t('Imported') }}</option>
<option :value="'not_imported'">{{ $t('Not imported') }}</option>
<option :value="'import_pending'">{{ $t('Import pending') }}</option>
</select>
</div>
</div>
</div>
<action-table
v-if="result"
:objects-data="result"
:actions="[['import', $t('Import')]]"
:action-url="'federation/library-tracks/action/'"
:filters="actionFilters">
<template slot="header-cells">
<th>{{ $t('Status') }}</th>
<th>{{ $t('Title') }}</th>
<th>{{ $t('Artist') }}</th>
<th>{{ $t('Album') }}</th>
<th>{{ $t('Published date') }}</th>
<th v-if="showLibrary">{{ $t('Library') }}</th>
</template>
<template slot="action-success-footer" slot-scope="scope">
<router-link
v-if="scope.result.action === 'import'"
:to="{name: 'library.import.batches.detail', params: {id: scope.result.result.batch.id }}">
{{ $t('Import #{% id %} launched', {id: scope.result.result.batch.id}) }}
</router-link>
</template>
<template slot="row-cells" slot-scope="scope">
<td>
<span v-if="scope.obj.local_track_file" class="ui basic green label">{{ $t('In library') }}</span>
<span v-else class="ui basic yellow label">{{ $t('Not imported') }}</span>
</td>
<td>
<span :title="scope.obj.title">{{ scope.obj.title|truncate(30) }}</span>
</td>
<td>
<span :title="scope.obj.artist_name">{{ scope.obj.artist_name|truncate(30) }}</span>
</td>
<td>
<span :title="scope.obj.album_title">{{ scope.obj.album_title|truncate(20) }}</span>
</td>
<td>
<human-date :date="scope.obj.published_date"></human-date>
</td>
<td v-if="showLibrary">
{{ scope.obj.library.actor.domain }}
</td>
</template>
</action-table>
<div class="dimmable">
<div v-if="isLoading" class="ui active inverted dimmer">
<div class="ui loader"></div>
</div>
<action-table
v-if="result"
@action-launched="fetchData"
:objects-data="result"
:actions="actions"
:action-url="'federation/library-tracks/action/'"
:filters="actionFilters">
<template slot="header-cells">
<th>{{ $t('Status') }}</th>
<th>{{ $t('Title') }}</th>
<th>{{ $t('Artist') }}</th>
<th>{{ $t('Album') }}</th>
<th>{{ $t('Published date') }}</th>
<th v-if="showLibrary">{{ $t('Library') }}</th>
</template>
<template slot="action-success-footer" slot-scope="scope">
<router-link
v-if="scope.result.action === 'import'"
:to="{name: 'library.import.batches.detail', params: {id: scope.result.result.batch.id }}">
{{ $t('Import #{% id %} launched', {id: scope.result.result.batch.id}) }}
</router-link>
</template>
<template slot="row-cells" slot-scope="scope">
<td>
<span v-if="scope.obj.status === 'imported'" class="ui basic green label">{{ $t('In library') }}</span>
<span v-else-if="scope.obj.status === 'import_pending'" class="ui basic yellow label">{{ $t('Import pending') }}</span>
<span v-else class="ui basic label">{{ $t('Not imported') }}</span>
</td>
<td>
<span :title="scope.obj.title">{{ scope.obj.title|truncate(30) }}</span>
</td>
<td>
<span :title="scope.obj.artist_name">{{ scope.obj.artist_name|truncate(30) }}</span>
</td>
<td>
<span :title="scope.obj.album_title">{{ scope.obj.album_title|truncate(20) }}</span>
</td>
<td>
<human-date :date="scope.obj.published_date"></human-date>
</td>
<td v-if="showLibrary">
{{ scope.obj.library.actor.domain }}
</td>
</template>
</action-table>
</div>
<div>
<pagination
v-if="result && result.results.length > 0"
......@@ -113,7 +121,7 @@ export default {
'q': this.search
}, this.filters)
if (this.importedFilter !== null) {
params.imported = this.importedFilter
params.status = this.importedFilter
}
let self = this
self.isLoading = true
......@@ -140,14 +148,21 @@ export default {
} else {
return currentFilters
}
},
actions () {
return [
{
name: 'import',
label: this.$t('Import'),
filterCheckable: (obj) => { return obj.status === 'not_imported' }
}
]
}
},
watch: {
search (newValue) {
if (newValue.length > 0) {
this.page = 1
this.fetchData()
}
this.page = 1
this.fetchData()
},
page () {
this.fetchData()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment