diff --git a/front/src/components/common/ActionTable.vue b/front/src/components/common/ActionTable.vue index b9822ca4de4ea57a38c3b43e5e581a20ef4bf207..718e57b19b61672913d0de6145ec7447c69ac0cf 100644 --- a/front/src/components/common/ActionTable.vue +++ b/front/src/components/common/ActionTable.vue @@ -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> diff --git a/front/src/components/common/DangerousButton.vue b/front/src/components/common/DangerousButton.vue index 690291d5b1d08abaa60f8576fce71b25a73f1047..52fcdca6136528bea7cb7b36125074654e62800e 100644 --- a/front/src/components/common/DangerousButton.vue +++ b/front/src/components/common/DangerousButton.vue @@ -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 diff --git a/front/src/components/federation/LibraryTrackTable.vue b/front/src/components/federation/LibraryTrackTable.vue index 551fb992a6518966b829b2ef9cdf2aff08caa356..43b52c835bb84ec3434f5a8fca110e70ecd24ff7 100644 --- a/front/src/components/federation/LibraryTrackTable.vue +++ b/front/src/components/federation/LibraryTrackTable.vue @@ -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()