fix: vuex mutation violation from draggable (#674)

Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/674
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
This commit is contained in:
dpschen 2021-08-23 19:24:52 +00:00 committed by konrad
parent 2adeb97074
commit 0a8505f53c
5 changed files with 81 additions and 37 deletions

View File

@ -78,8 +78,13 @@
class="more-container" class="more-container"
v-if="typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true" v-if="typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true"
> >
<!--
NOTE: a v-model / computed setter is not possible, since the updateActiveLists function
triggered by the change needs to have access to the current namespace
-->
<draggable <draggable
v-model="n.lists" :value="activeLists[nk]"
@input="(lists) => updateActiveLists(n, lists)"
:group="`namespace-${n.id}-lists`" :group="`namespace-${n.id}-lists`"
@start="() => drag = true" @start="() => drag = true"
@end="e => saveListPosition(e, nk)" @end="e => saveListPosition(e, nk)"
@ -94,11 +99,9 @@
tag="ul" tag="ul"
class="menu-list can-be-hidden" class="menu-list can-be-hidden"
> >
<!-- eslint-disable vue/no-use-v-if-with-v-for,vue/no-confusing-v-for-v-if -->
<li <li
v-for="l in n.lists" v-for="l in activeLists[nk]"
:key="l.id" :key="l.id"
v-if="!l.isArchived"
class="loader-container" class="loader-container"
:class="{'is-loading': listUpdating[l.id]}" :class="{'is-loading': listUpdating[l.id]}"
> >
@ -167,13 +170,18 @@ export default {
NamespaceSettingsDropdown, NamespaceSettingsDropdown,
draggable, draggable,
}, },
computed: mapState({ computed: {
namespaces: state => state.namespaces.namespaces.filter(n => !n.isArchived), ...mapState({
currentList: CURRENT_LIST, namespaces: state => state.namespaces.namespaces.filter(n => !n.isArchived),
background: 'background', currentList: CURRENT_LIST,
menuActive: MENU_ACTIVE, background: 'background',
loading: state => state[LOADING] && state[LOADING_MODULE] === 'namespaces', menuActive: MENU_ACTIVE,
}), loading: state => state[LOADING] && state[LOADING_MODULE] === 'namespaces',
}),
activeLists() {
return this.namespaces.map(({lists}) => lists.filter(item => !item.isArchived))
},
},
beforeCreate() { beforeCreate() {
this.$store.dispatch('namespaces/loadNamespaces') this.$store.dispatch('namespaces/loadNamespaces')
.then(namespaces => { .then(namespaces => {
@ -211,16 +219,38 @@ export default {
toggleLists(namespaceId) { toggleLists(namespaceId) {
this.$set(this.listsVisible, namespaceId, !this.listsVisible[namespaceId] ?? false) this.$set(this.listsVisible, namespaceId, !this.listsVisible[namespaceId] ?? false)
}, },
updateActiveLists(namespace, activeLists) {
// this is a bit hacky: since we do have to filter out the archived items from the list
// for vue draggable updating it is not as simple as replacing it.
// instead we iterate over the non archived items in the old list and replace them with the ones in their new order
const lists = namespace.lists.map((item) => {
if (item.isArchived) {
return item
}
return activeLists.shift()
})
const newNamespace = {
...namespace,
lists,
}
this.$store.commit('namespaces/setNamespaceById', newNamespace)
},
saveListPosition(e, namespaceIndex) { saveListPosition(e, namespaceIndex) {
const listsFiltered = this.namespaces[namespaceIndex].lists.filter(l => !l.isArchived) const listsActive = this.activeLists[namespaceIndex]
const list = listsFiltered[e.newIndex] const list = listsActive[e.newIndex]
const listBefore = listsFiltered[e.newIndex - 1] ?? null const listBefore = listsActive[e.newIndex - 1] ?? null
const listAfter = listsFiltered[e.newIndex + 1] ?? null const listAfter = listsActive[e.newIndex + 1] ?? null
this.$set(this.listUpdating, list.id, true) this.$set(this.listUpdating, list.id, true)
list.position = calculateItemPosition(listBefore !== null ? listBefore.position : null, listAfter !== null ? listAfter.position : null) const position = calculateItemPosition(listBefore !== null ? listBefore.position : null, listAfter !== null ? listAfter.position : null)
this.$store.dispatch('lists/updateList', list) // create a copy of the list in order to not violate vuex mutations
this.$store.dispatch('lists/updateList', {
...list,
position,
})
.catch(e => { .catch(e => {
this.error(e) this.error(e)
}) })

View File

@ -39,6 +39,11 @@ export default class ListService extends AbstractService {
return list return list
} }
update(model) {
const newModel = { ... model }
return super.update(newModel)
}
background(list) { background(list) {
if (list.background === null) { if (list.background === null) {
return Promise.resolve('') return Promise.resolve('')

View File

@ -24,6 +24,7 @@ import ListService from '../services/list'
Vue.use(Vuex) Vue.use(Vuex)
export const store = new Vuex.Store({ export const store = new Vuex.Store({
strict: import.meta.env.DEV,
modules: { modules: {
config, config,
auth, auth,

View File

@ -38,9 +38,10 @@ export default {
}, },
actions: { actions: {
toggleListFavorite(ctx, list) { toggleListFavorite(ctx, list) {
list.isFavorite = !list.isFavorite return ctx.dispatch('updateList', {
...list,
return ctx.dispatch('updateList', list) isFavorite: !list.isFavorite,
})
}, },
createList(ctx, list) { createList(ctx, list) {
const cancel = setLoading(ctx, 'lists') const cancel = setLoading(ctx, 'lists')
@ -61,24 +62,31 @@ export default {
const listService = new ListService() const listService = new ListService()
return listService.update(list) return listService.update(list)
.then(r => { .then(() => {
ctx.commit('setList', r) ctx.commit('setList', list)
ctx.commit('namespaces/setListInNamespaceById', r, {root: true}) ctx.commit('namespaces/setListInNamespaceById', list, {root: true})
if (r.isFavorite) {
r.namespaceId = FavoriteListsNamespace // the returned list from listService.update is the same!
ctx.commit('namespaces/addListToNamespace', r, {root: true}) // in order to not validate vuex mutations we have to create a new copy
const newList = {
...list,
namespaceId: FavoriteListsNamespace,
}
if (list.isFavorite) {
ctx.commit('namespaces/addListToNamespace', newList, {root: true})
} else { } else {
r.namespaceId = FavoriteListsNamespace ctx.commit('namespaces/removeListFromNamespaceById', newList, {root: true})
ctx.commit('namespaces/removeListFromNamespaceById', r, {root: true})
} }
ctx.dispatch('namespaces/loadNamespacesIfFavoritesDontExist', null, {root: true}) ctx.dispatch('namespaces/loadNamespacesIfFavoritesDontExist', null, {root: true})
ctx.dispatch('namespaces/removeFavoritesNamespaceIfEmpty', null, {root: true}) ctx.dispatch('namespaces/removeFavoritesNamespaceIfEmpty', null, {root: true})
return Promise.resolve(r) return Promise.resolve(newList)
}) })
.catch(e => { .catch(e => {
// Reset the list state to the initial one to avoid confusion for the user // Reset the list state to the initial one to avoid confusion for the user
list.isFavorite = !list.isFavorite ctx.commit('setList', {
ctx.commit('setList', list) ...list,
isFavorite: !list.isFavorite,
})
return Promise.reject(e) return Promise.reject(e)
}) })
.finally(() => cancel()) .finally(() => cancel())

View File

@ -13,13 +13,13 @@ export default {
state.namespaces = namespaces state.namespaces = namespaces
}, },
setNamespaceById(state, namespace) { setNamespaceById(state, namespace) {
for (const n in state.namespaces) { const namespaceIndex = state.namespaces.findIndex(n => n.id === namespace.id)
if (state.namespaces[n].id === namespace.id) {
namespace.lists = state.namespaces[n].lists if (namespaceIndex === -1) {
Vue.set(state.namespaces, n, namespace) return
return
}
} }
Vue.set(state.namespaces, namespaceIndex, namespace)
}, },
setListInNamespaceById(state, list) { setListInNamespaceById(state, list) {
for (const n in state.namespaces) { for (const n in state.namespaces) {