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

This commit is contained in:
Eliot Berriot 2018-05-24 20:07:14 +02:00
parent eded32c2e8
commit 6586b2b73d
No known key found for this signature in database
GPG Key ID: DD6965E2476E5C27
3 changed files with 159 additions and 70 deletions

View File

@ -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>&nbsp;</label> :disabled="checkable.length === 0"
:checked="checkable.length > 0 && checked.length === checkable.length"><label>&nbsp;</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>&nbsp;</label> :checked="checked.indexOf(obj.id) > -1"><label>&nbsp;</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>

View File

@ -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

View File

@ -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()