See #228: smarter action table with shift-click select
This commit is contained in:
parent
eded32c2e8
commit
6586b2b73d
|
@ -1,29 +1,42 @@
|
||||||
<template>
|
<template>
|
||||||
<table class="ui compact very basic single line unstackable table">
|
<table class="ui compact very basic single line unstackable table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr v-if="actions.length > 0 && objectsData.count > 0">
|
<tr v-if="actions.length > 0">
|
||||||
<th colspan="1000">
|
<th colspan="1000">
|
||||||
<div class="ui small form">
|
<div class="ui small form">
|
||||||
<div class="ui inline fields">
|
<div class="ui inline fields">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ $t('Actions') }}</label>
|
<label>{{ $t('Actions') }}</label>
|
||||||
<select class="ui dropdown" v-model="currentAction">
|
<select class="ui dropdown" v-model="currentActionName">
|
||||||
<option v-for="action in actions" :value="action[0]">
|
<option v-for="action in actions" :value="action.name">
|
||||||
{{ action[1] }}
|
{{ action.label }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div
|
<div
|
||||||
|
v-if="!selectAll"
|
||||||
@click="launchAction"
|
@click="launchAction"
|
||||||
:disabled="checked.length === 0"
|
:disabled="checked.length === 0"
|
||||||
:class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']">
|
:class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']">
|
||||||
{{ $t('Go') }}</div>
|
{{ $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>
|
||||||
<div class="count field">
|
<div class="count field">
|
||||||
<span v-if="selectAll">{{ $t('{% count %} on {% total %} selected', {count: objectsData.count, total: objectsData.count}) }}</span>
|
<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>
|
<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">
|
<a @click="selectAll = true" v-if="!selectAll">
|
||||||
{{ $t('Select all {% total %} elements', {total: objectsData.count}) }}
|
{{ $t('Select all {% total %} elements', {total: objectsData.count}) }}
|
||||||
</a>
|
</a>
|
||||||
|
@ -53,18 +66,20 @@
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@change="toggleCheckAll"
|
@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>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<slot name="header-cells"></slot>
|
<slot name="header-cells"></slot>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody v-if="objectsData.count > 0">
|
||||||
<tr v-for="obj in objectsData.results">
|
<tr v-for="(obj, index) in objectsData.results">
|
||||||
<td class="collapsing">
|
<td class="collapsing">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
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>
|
:checked="checked.indexOf(obj.id) > -1"><label> </label>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -90,38 +105,60 @@ export default {
|
||||||
actionLoading: false,
|
actionLoading: false,
|
||||||
actionResult: null,
|
actionResult: null,
|
||||||
actionErrors: [],
|
actionErrors: [],
|
||||||
currentAction: null,
|
currentActionName: null,
|
||||||
selectAll: false
|
selectAll: false,
|
||||||
|
lastCheckedIndex: -1
|
||||||
}
|
}
|
||||||
if (this.actions.length > 0) {
|
if (this.actions.length > 0) {
|
||||||
d.currentAction = this.actions[0][0]
|
d.currentActionName = this.actions[0].name
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleCheckAll () {
|
toggleCheckAll () {
|
||||||
if (this.checked.length === this.objectsData.results.length) {
|
this.lastCheckedIndex = -1
|
||||||
|
if (this.checked.length === this.checkable.length) {
|
||||||
// we uncheck
|
// we uncheck
|
||||||
this.checked = []
|
this.checked = []
|
||||||
} else {
|
} 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) {
|
if (this.checked.indexOf(id) > -1) {
|
||||||
// we uncheck
|
// we uncheck
|
||||||
this.selectAll = false
|
this.selectAll = false
|
||||||
this.checked.splice(this.checked.indexOf(id), 1)
|
newValue = false
|
||||||
} else {
|
} 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 () {
|
launchAction () {
|
||||||
let self = this
|
let self = this
|
||||||
self.actionLoading = true
|
self.actionLoading = true
|
||||||
self.result = null
|
self.result = null
|
||||||
let payload = {
|
let payload = {
|
||||||
action: this.currentAction,
|
action: this.currentActionName,
|
||||||
filters: this.filters
|
filters: this.filters
|
||||||
}
|
}
|
||||||
if (this.selectAll) {
|
if (this.selectAll) {
|
||||||
|
@ -132,11 +169,39 @@ export default {
|
||||||
axios.post(this.actionUrl, payload).then((response) => {
|
axios.post(this.actionUrl, payload).then((response) => {
|
||||||
self.actionResult = response.data
|
self.actionResult = response.data
|
||||||
self.actionLoading = false
|
self.actionLoading = false
|
||||||
|
self.$emit('action-launched', response.data)
|
||||||
}, error => {
|
}, error => {
|
||||||
self.actionLoading = false
|
self.actionLoading = false
|
||||||
self.actionErrors = error.backendErrors
|
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>
|
</script>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<div class="ui cancel button"><i18next path="Cancel"/></div>
|
<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>
|
<slot name="modal-confirm"><i18next path="Confirm"/></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,7 +28,8 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
action: {type: Function, required: false},
|
action: {type: Function, required: false},
|
||||||
disabled: {type: Boolean, default: false},
|
disabled: {type: Boolean, default: false},
|
||||||
color: {type: String, default: 'red'}
|
color: {type: String, default: 'red'},
|
||||||
|
confirmColor: {type: String, default: null, required: false}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Modal
|
Modal
|
||||||
|
@ -38,6 +39,14 @@ export default {
|
||||||
showModal: false
|
showModal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
confirmButtonColor () {
|
||||||
|
if (this.confirmColor) {
|
||||||
|
return this.confirmColor
|
||||||
|
}
|
||||||
|
return this.color
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
confirm () {
|
confirm () {
|
||||||
this.showModal = false
|
this.showModal = false
|
||||||
|
|
|
@ -10,16 +10,22 @@
|
||||||
<label>{{ $t('Import status') }}</label>
|
<label>{{ $t('Import status') }}</label>
|
||||||
<select class="ui dropdown" v-model="importedFilter">
|
<select class="ui dropdown" v-model="importedFilter">
|
||||||
<option :value="null">{{ $t('Any') }}</option>
|
<option :value="null">{{ $t('Any') }}</option>
|
||||||
<option :value="true">{{ $t('Imported') }}</option>
|
<option :value="'imported'">{{ $t('Imported') }}</option>
|
||||||
<option :value="false">{{ $t('Not imported') }}</option>
|
<option :value="'not_imported'">{{ $t('Not imported') }}</option>
|
||||||
|
<option :value="'import_pending'">{{ $t('Import pending') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="dimmable">
|
||||||
|
<div v-if="isLoading" class="ui active inverted dimmer">
|
||||||
|
<div class="ui loader"></div>
|
||||||
|
</div>
|
||||||
<action-table
|
<action-table
|
||||||
v-if="result"
|
v-if="result"
|
||||||
|
@action-launched="fetchData"
|
||||||
:objects-data="result"
|
:objects-data="result"
|
||||||
:actions="[['import', $t('Import')]]"
|
:actions="actions"
|
||||||
:action-url="'federation/library-tracks/action/'"
|
:action-url="'federation/library-tracks/action/'"
|
||||||
:filters="actionFilters">
|
:filters="actionFilters">
|
||||||
<template slot="header-cells">
|
<template slot="header-cells">
|
||||||
|
@ -39,8 +45,9 @@
|
||||||
</template>
|
</template>
|
||||||
<template slot="row-cells" slot-scope="scope">
|
<template slot="row-cells" slot-scope="scope">
|
||||||
<td>
|
<td>
|
||||||
<span v-if="scope.obj.local_track_file" class="ui basic green label">{{ $t('In library') }}</span>
|
<span v-if="scope.obj.status === 'imported'" class="ui basic green label">{{ $t('In library') }}</span>
|
||||||
<span v-else class="ui basic yellow label">{{ $t('Not imported') }}</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>
|
||||||
<td>
|
<td>
|
||||||
<span :title="scope.obj.title">{{ scope.obj.title|truncate(30) }}</span>
|
<span :title="scope.obj.title">{{ scope.obj.title|truncate(30) }}</span>
|
||||||
|
@ -59,6 +66,7 @@
|
||||||
</td>
|
</td>
|
||||||
</template>
|
</template>
|
||||||
</action-table>
|
</action-table>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<pagination
|
<pagination
|
||||||
v-if="result && result.results.length > 0"
|
v-if="result && result.results.length > 0"
|
||||||
|
@ -113,7 +121,7 @@ export default {
|
||||||
'q': this.search
|
'q': this.search
|
||||||
}, this.filters)
|
}, this.filters)
|
||||||
if (this.importedFilter !== null) {
|
if (this.importedFilter !== null) {
|
||||||
params.imported = this.importedFilter
|
params.status = this.importedFilter
|
||||||
}
|
}
|
||||||
let self = this
|
let self = this
|
||||||
self.isLoading = true
|
self.isLoading = true
|
||||||
|
@ -140,14 +148,21 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
return currentFilters
|
return currentFilters
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
actions () {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'import',
|
||||||
|
label: this.$t('Import'),
|
||||||
|
filterCheckable: (obj) => { return obj.status === 'not_imported' }
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
search (newValue) {
|
search (newValue) {
|
||||||
if (newValue.length > 0) {
|
|
||||||
this.page = 1
|
this.page = 1
|
||||||
this.fetchData()
|
this.fetchData()
|
||||||
}
|
|
||||||
},
|
},
|
||||||
page () {
|
page () {
|
||||||
this.fetchData()
|
this.fetchData()
|
||||||
|
|
Loading…
Reference in New Issue