See #228: smarter action table with shift-click select
This commit is contained in:
parent
eded32c2e8
commit
6586b2b73d
|
@ -1,29 +1,42 @@
|
|||
<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> </label>
|
||||
:disabled="checkable.length === 0"
|
||||
:checked="checkable.length > 0 && checked.length === checkable.length"><label> </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> </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()
|
||||
|
|
Loading…
Reference in New Issue