feat: move list store to pina (#2392)

Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2392
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
This commit is contained in:
Dominik Pschenitschni 2022-09-21 16:21:25 +00:00 committed by konrad
parent f85a08afb4
commit a38075f376
23 changed files with 272 additions and 239 deletions

View file

@ -124,7 +124,7 @@
<BaseButton <BaseButton
class="favorite" class="favorite"
:class="{'is-favorite': l.isFavorite}" :class="{'is-favorite': l.isFavorite}"
@click="toggleFavoriteList(l)" @click="listStore.toggleListFavorite(l)"
> >
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/> <icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
</BaseButton> </BaseButton>
@ -159,6 +159,7 @@ import {useEventListener} from '@vueuse/core'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
import type {INamespace} from '@/modelTypes/INamespace' import type {INamespace} from '@/modelTypes/INamespace'
import ColorBubble from '@/components/misc/colorBubble.vue' import ColorBubble from '@/components/misc/colorBubble.vue'
import {useListStore} from '@/stores/lists'
const drag = ref(false) const drag = ref(false)
const dragOptions = { const dragOptions = {
@ -195,15 +196,7 @@ const namespaceListsCount = computed(() => {
useEventListener('resize', resize) useEventListener('resize', resize)
onMounted(() => resize()) onMounted(() => resize())
const listStore = useListStore()
function toggleFavoriteList(list: IList) {
// The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite
if (list.id === -1 || list.isArchived) {
return
}
store.dispatch('lists/toggleListFavorite', list)
}
function resize() { function resize() {
// Hide the menu by default on mobile // Hide the menu by default on mobile
@ -268,7 +261,7 @@ async function saveListPosition(e: SortableEvent) {
try { try {
// create a copy of the list in order to not violate vuex mutations // create a copy of the list in order to not violate vuex mutations
await store.dispatch('lists/updateList', { await listStore.updateList({
...list, ...list,
position, position,
namespaceId, namespaceId,

View file

@ -24,7 +24,7 @@
<BaseButton <BaseButton
v-else v-else
:class="{'is-favorite': list.isFavorite}" :class="{'is-favorite': list.isFavorite}"
@click.stop="toggleFavoriteList(list)" @click.stop="listStore.toggleListFavorite(list)"
class="favorite" class="favorite"
> >
<icon :icon="list.isFavorite ? 'star' : ['far', 'star']"/> <icon :icon="list.isFavorite ? 'star' : ['far', 'star']"/>
@ -37,7 +37,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import {type PropType, ref, watch} from 'vue' import {type PropType, ref, watch} from 'vue'
import {useStore} from '@/store'
import ListService from '@/services/list' import ListService from '@/services/list'
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash' import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
@ -46,6 +45,7 @@ import {colorIsDark} from '@/helpers/color/colorIsDark'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
import {useListStore} from '@/stores/lists'
const background = ref<string | null>(null) const background = ref<string | null>(null)
const backgroundLoading = ref(false) const backgroundLoading = ref(false)
@ -84,16 +84,7 @@ async function loadBackground() {
} }
} }
const store = useStore() const listStore = useListStore()
function toggleFavoriteList(list: IList) {
// The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite
if (list.id === -1 || list.isArchived) {
return
}
store.dispatch('lists/toggleListFavorite', list)
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -70,6 +70,7 @@ import {getHistory} from '@/modules/listHistory'
import {parseTaskText, PrefixMode} from '@/modules/parseTaskText' import {parseTaskText, PrefixMode} from '@/modules/parseTaskText'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode' import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {PREFIXES} from '@/modules/parseTaskText' import {PREFIXES} from '@/modules/parseTaskText'
import {useListStore} from '@/stores/lists'
const TYPE_LIST = 'list' const TYPE_LIST = 'list'
const TYPE_TASK = 'task' const TYPE_TASK = 'task'
@ -116,6 +117,8 @@ export default defineComponent({
}, },
results() { results() {
let lists = [] let lists = []
const listStore = useListStore()
if (this.searchMode === SEARCH_MODE_ALL || this.searchMode === SEARCH_MODE_LISTS) { if (this.searchMode === SEARCH_MODE_ALL || this.searchMode === SEARCH_MODE_LISTS) {
const {list} = this.parsedQuery const {list} = this.parsedQuery
@ -126,10 +129,8 @@ export default defineComponent({
const history = getHistory() const history = getHistory()
// Puts recently visited lists at the top // Puts recently visited lists at the top
const allLists = [...new Set([ const allLists = [...new Set([
...history.map(l => { ...history.map(l => listStore.getListById(l.id)),
return this.$store.getters['lists/getListById'](l.id) ...listStore.searchList(list),
}),
...this.$store.getters['lists/searchList'](list),
])] ])]
lists = allLists.filter(l => { lists = allLists.filter(l => {
@ -296,8 +297,10 @@ export default defineComponent({
filter_comparator: [], filter_comparator: [],
} }
const listStore = useListStore()
if (list !== null) { if (list !== null) {
const l = this.$store.getters['lists/findListByExactname'](list) const l = listStore.findListByExactname(list)
if (l !== null) { if (l !== null) {
params.filter_by.push('list_id') params.filter_by.push('list_id')
params.filter_value.push(l.id) params.filter_value.push(l.id)
@ -318,7 +321,7 @@ export default defineComponent({
const r = await this.taskService.getAll({}, params) const r = await this.taskService.getAll({}, params)
this.foundTasks = r.map(t => { this.foundTasks = r.map(t => {
t.type = TYPE_TASK t.type = TYPE_TASK
const list = this.$store.getters['lists/getListById'](t.listId) const list = listStore.getListById(t.listId)
if (list !== null) { if (list !== null) {
t.title = `${t.title} (${list.title})` t.title = `${t.title} (${list.title})`
} }
@ -424,7 +427,8 @@ export default defineComponent({
title: this.query, title: this.query,
namespaceId: this.currentList.namespaceId, namespaceId: this.currentList.namespaceId,
}) })
const list = await this.$store.dispatch('lists/createList', newList) const listStore = useListStore()
const list = await listStore.createList(newList)
this.$message.success({message: this.$t('list.create.createdSuccess')}) this.$message.success({message: this.$t('list.create.createdSuccess')})
this.$router.push({name: 'list.index', params: {listId: list.id}}) this.$router.push({name: 'list.index', params: {listId: list.id}})
this.closeQuickActions() this.closeQuickActions()

View file

@ -24,6 +24,7 @@ import {useI18n} from 'vue-i18n'
import ListModel from '@/models/list' import ListModel from '@/models/list'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
import {useListStore} from '@/stores/lists'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -34,6 +35,7 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const store = useStore() const store = useStore()
const listStore = useListStore()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const list: IList = reactive(new ListModel()) const list: IList = reactive(new ListModel())
@ -47,12 +49,12 @@ watch(
}, },
) )
const foundLists = ref([]) const foundLists = ref<IList[]>([])
function findLists(query: string) { function findLists(query: string) {
if (query === '') { if (query === '') {
select(null) select(null)
} }
foundLists.value = store.getters['lists/searchList'](query) foundLists.value = listStore.searchList(query)
} }
function select(l: IList | null) { function select(l: IList | null) {

View file

@ -15,9 +15,9 @@
:to="{ name: 'list.list', params: { listId: task.listId } }" :to="{ name: 'list.list', params: { listId: task.listId } }"
class="task-list" class="task-list"
:class="{'mr-2': task.hexColor !== ''}" :class="{'mr-2': task.hexColor !== ''}"
v-if="showList && $store.getters['lists/getListById'](task.listId) !== null" v-if="showList && getListById(task.listId) !== null"
v-tooltip="$t('task.detail.belongsToList', {list: $store.getters['lists/getListById'](task.listId).title})"> v-tooltip="$t('task.detail.belongsToList', {list: getListById(task.listId).title})">
{{ $store.getters['lists/getListById'](task.listId).title }} {{ getListById(task.listId).title }}
</router-link> </router-link>
<ColorBubble <ColorBubble
@ -85,9 +85,9 @@
<router-link <router-link
:to="{ name: 'list.list', params: { listId: task.listId } }" :to="{ name: 'list.list', params: { listId: task.listId } }"
class="task-list" class="task-list"
v-if="!showList && currentList.id !== task.listId && $store.getters['lists/getListById'](task.listId) !== null" v-if="!showList && currentList.id !== task.listId && getListById(task.listId) !== null"
v-tooltip="$t('task.detail.belongsToList', {list: $store.getters['lists/getListById'](task.listId).title})"> v-tooltip="$t('task.detail.belongsToList', {list: getListById(task.listId).title})">
{{ $store.getters['lists/getListById'](task.listId).title }} {{ getListById(task.listId).title }}
</router-link> </router-link>
<BaseButton <BaseButton
:class="{'is-favorite': task.isFavorite}" :class="{'is-favorite': task.isFavorite}"
@ -102,6 +102,7 @@
<script lang="ts"> <script lang="ts">
import {defineComponent, type PropType} from 'vue' import {defineComponent, type PropType} from 'vue'
import {mapState} from 'pinia'
import TaskModel from '@/models/task' import TaskModel from '@/models/task'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
@ -117,6 +118,7 @@ import {playPop} from '@/helpers/playPop'
import ChecklistSummary from './checklist-summary.vue' import ChecklistSummary from './checklist-summary.vue'
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate' import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
import ColorBubble from '@/components/misc/colorBubble.vue' import ColorBubble from '@/components/misc/colorBubble.vue'
import {useListStore} from '@/stores/lists'
export default defineComponent({ export default defineComponent({
name: 'singleTaskInList', name: 'singleTaskInList',
@ -177,8 +179,11 @@ export default defineComponent({
document.removeEventListener('click', this.hideDeferDueDatePopup) document.removeEventListener('click', this.hideDeferDueDatePopup)
}, },
computed: { computed: {
...mapState(useListStore, {
getListById: 'getListById',
}),
listColor() { listColor() {
const list = this.$store.getters['lists/getListById'](this.task.listId) const list = this.getListById(this.task.listId)
return list !== null ? list.hexColor : '' return list !== null ? list.hexColor : ''
}, },
currentList() { currentList() {

View file

@ -1,37 +0,0 @@
import {watch, reactive, shallowReactive, unref, toRefs, readonly} from 'vue'
import type {MaybeRef} from '@vueuse/core'
import {useStore} from '@/store'
import ListService from '@/services/list'
import ListModel from '@/models/list'
import { success } from '@/message'
import {useI18n} from 'vue-i18n'
export function useList(listId: MaybeRef<ListModel['id']>) {
const listService = shallowReactive(new ListService())
const {loading: isLoading} = toRefs(listService)
const list : ListModel = reactive(new ListModel({}))
const {t} = useI18n({useScope: 'global'})
watch(
() => unref(listId),
async (listId) => {
const loadedList = await listService.get(new ListModel({id: listId}))
Object.assign(list, loadedList)
},
{immediate: true},
)
const store = useStore()
async function save() {
await store.dispatch('lists/updateList', list)
success({message: t('list.edit.success')})
}
return {
isLoading: readonly(isLoading),
list,
save,
}
}

View file

@ -8,6 +8,9 @@ import type ListModel from '@/models/list'
import {saveListView, getListView} from '@/helpers/saveListView' import {saveListView, getListView} from '@/helpers/saveListView'
import {parseDateOrString} from '@/helpers/time/parseDateOrString' import {parseDateOrString} from '@/helpers/time/parseDateOrString'
import {getNextWeekDate} from '@/helpers/time/getNextWeekDate' import {getNextWeekDate} from '@/helpers/time/getNextWeekDate'
import {setTitle} from '@/helpers/setTitle'
import {useListStore} from '@/stores/lists'
import HomeComponent from '../views/Home.vue' import HomeComponent from '../views/Home.vue'
import NotFoundComponent from '../views/404.vue' import NotFoundComponent from '../views/404.vue'
@ -55,7 +58,6 @@ import NamespaceSettingDelete from '../views/namespaces/settings/delete.vue'
import FilterNew from '@/views/filters/FilterNew.vue' import FilterNew from '@/views/filters/FilterNew.vue'
import FilterEdit from '@/views/filters/FilterEdit.vue' import FilterEdit from '@/views/filters/FilterEdit.vue'
import FilterDelete from '@/views/filters/FilterDelete.vue' import FilterDelete from '@/views/filters/FilterDelete.vue'
import {setTitle} from '@/helpers/setTitle'
const PasswordResetComponent = () => import('../views/user/PasswordReset.vue') const PasswordResetComponent = () => import('../views/user/PasswordReset.vue')
const GetPasswordResetComponent = () => import('../views/user/RequestPasswordReset.vue') const GetPasswordResetComponent = () => import('../views/user/RequestPasswordReset.vue')
@ -392,7 +394,8 @@ const router = createRouter({
beforeEnter: (to) => { beforeEnter: (to) => {
saveListView(to.params.listId, to.name) saveListView(to.params.listId, to.name)
// Properly set the page title when a task popup is closed // Properly set the page title when a task popup is closed
const listFromStore = store.getters['lists/getListById'](parseInt(to.params.listId)) const listStore = useListStore()
const listFromStore = listStore.getListById(Number(to.params.listId))
if(listFromStore) { if(listFromStore) {
setTitle(listFromStore.title) setTitle(listFromStore.title)
} }

View file

@ -18,7 +18,6 @@ import auth from './modules/auth'
import namespaces from './modules/namespaces' import namespaces from './modules/namespaces'
import kanban from './modules/kanban' import kanban from './modules/kanban'
import tasks from './modules/tasks' import tasks from './modules/tasks'
import lists from './modules/lists'
import attachments from './modules/attachments' import attachments from './modules/attachments'
import ListModel from '@/models/list' import ListModel from '@/models/list'
@ -43,7 +42,6 @@ export const store = createStore<RootStoreState>({
namespaces, namespaces,
kanban, kanban,
tasks, tasks,
lists,
attachments, attachments,
}, },
state: () => ({ state: () => ({

View file

@ -1,130 +0,0 @@
import type { Module } from 'vuex'
import ListService from '@/services/list'
import {setLoading} from '@/store/helper'
import {removeListFromHistory} from '@/modules/listHistory'
import {createNewIndexer} from '@/indexes'
import type {ListState, RootStoreState} from '@/store/types'
import type {IList} from '@/modelTypes/IList'
const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description'])
const FavoriteListsNamespace = -2
const listsStore : Module<ListState, RootStoreState>= {
namespaced: true,
// The state is an object which has the list ids as keys.
state: () => ({}),
mutations: {
setList(state, list: IList) {
state[list.id] = list
update(list)
},
setLists(state, lists: IList[]) {
lists.forEach(l => {
state[l.id] = l
add(l)
})
},
removeListById(state, list: IList) {
remove(list)
delete state[list.id]
},
},
getters: {
getListById: (state) => (id: IList['id']) => {
if (typeof state[id] !== 'undefined') {
return state[id]
}
return null
},
findListByExactname: (state) => (name: string) => {
const list = Object.values(state).find(l => {
return l.title.toLowerCase() === name.toLowerCase()
})
return typeof list === 'undefined' ? null : list
},
searchList: (state) => (query: string, includeArchived = false) => {
return search(query)
?.filter(value => value > 0)
.map(id => state[id])
.filter(list => list.isArchived === includeArchived)
|| []
},
},
actions: {
toggleListFavorite(ctx, list: IList) {
return ctx.dispatch('updateList', {
...list,
isFavorite: !list.isFavorite,
})
},
async createList(ctx, list: IList) {
const cancel = setLoading(ctx, 'lists')
const listService = new ListService()
try {
const createdList = await listService.create(list)
createdList.namespaceId = list.namespaceId
ctx.commit('namespaces/addListToNamespace', createdList, {root: true})
ctx.commit('setList', createdList)
return createdList
} finally {
cancel()
}
},
async updateList(ctx, list: IList) {
const cancel = setLoading(ctx, 'lists')
const listService = new ListService()
try {
await listService.update(list)
ctx.commit('setList', list)
ctx.commit('namespaces/setListInNamespaceById', list, {root: true})
// the returned list from listService.update is the same!
// 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 {
ctx.commit('namespaces/removeListFromNamespaceById', newList, {root: true})
}
ctx.dispatch('namespaces/loadNamespacesIfFavoritesDontExist', null, {root: true})
ctx.dispatch('namespaces/removeFavoritesNamespaceIfEmpty', null, {root: true})
return newList
} catch (e) {
// Reset the list state to the initial one to avoid confusion for the user
ctx.commit('setList', {
...list,
isFavorite: !list.isFavorite,
})
throw e
} finally {
cancel()
}
},
async deleteList(ctx, list: IList) {
const cancel = setLoading(ctx, 'lists')
const listService = new ListService()
try {
const response = await listService.delete(list)
ctx.commit('removeListById', list)
ctx.commit('namespaces/removeListFromNamespaceById', list, {root: true})
removeListFromHistory({id: list.id})
return response
} finally {
cancel()
}
},
},
}
export default listsStore

View file

@ -6,6 +6,7 @@ import {createNewIndexer} from '@/indexes'
import type {NamespaceState, RootStoreState} from '@/store/types' import type {NamespaceState, RootStoreState} from '@/store/types'
import type {INamespace} from '@/modelTypes/INamespace' import type {INamespace} from '@/modelTypes/INamespace'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
import {useListStore} from '@/stores/lists'
const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description']) const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description'])
@ -130,7 +131,8 @@ const namespacesStore : Module<NamespaceState, RootStoreState> = {
// Put all lists in the list state // Put all lists in the list state
const lists = namespaces.flatMap(({lists}) => lists) const lists = namespaces.flatMap(({lists}) => lists)
ctx.commit('lists/setLists', lists, {root: true}) const listStore = useListStore()
listStore.setLists(lists)
return namespaces return namespaces
} finally { } finally {

View file

@ -26,6 +26,7 @@ import type { IList } from '@/modelTypes/IList'
import type { RootStoreState, TaskState } from '@/store/types' import type { RootStoreState, TaskState } from '@/store/types'
import { useLabelStore } from '@/stores/labels' import { useLabelStore } from '@/stores/labels'
import { useListStore } from '@/stores/lists'
// IDEA: maybe use a small fuzzy search here to prevent errors // IDEA: maybe use a small fuzzy search here to prevent errors
function findPropertyByValue(object, key, value) { function findPropertyByValue(object, key, value) {
@ -292,7 +293,7 @@ const tasksStore : Module<TaskState, RootStoreState>= {
return task return task
}, },
findListId({ rootGetters }, { list: listName, listId }: { findListId(_, { list: listName, listId }: {
list: string, list: string,
listId: IList['id'] listId: IList['id']
}) { }) {
@ -301,7 +302,8 @@ const tasksStore : Module<TaskState, RootStoreState>= {
// Uses the following ways to get the list id of the new task: // Uses the following ways to get the list id of the new task:
// 1. If specified in quick add magic, look in store if it exists and use it if it does // 1. If specified in quick add magic, look in store if it exists and use it if it does
if (listName !== null) { if (listName !== null) {
const list = rootGetters['lists/findListByExactname'](listName) const listStore = useListStore()
const list = listStore.findListByExactname(listName)
foundListId = list === null ? null : list.id foundListId = list === null ? null : list.id
} }

View file

@ -97,7 +97,8 @@ export interface LabelState {
} }
export interface ListState { export interface ListState {
[id: IList['id']]: IList lists: { [id: IList['id']]: IList },
isLoading: boolean,
} }
export interface NamespaceState { export interface NamespaceState {

182
src/stores/lists.ts Normal file
View file

@ -0,0 +1,182 @@
import {watch, reactive, shallowReactive, unref, toRefs, readonly} from 'vue'
import {defineStore} from 'pinia'
import {useI18n} from 'vue-i18n'
import ListService from '@/services/list'
import {setLoadingPinia} from '@/store/helper'
import {removeListFromHistory} from '@/modules/listHistory'
import {createNewIndexer} from '@/indexes'
import {store as vuexStore} from '@/store' // for gradual conversion, see fullUserDetails
import type {ListState} from '@/store/types'
import type {IList} from '@/modelTypes/IList'
import type {MaybeRef} from '@vueuse/core'
import ListModel from '@/models/list'
import {success} from '@/message'
const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description'])
const FavoriteListsNamespace = -2
export const useListStore = defineStore('list', {
state: () : ListState => ({
isLoading: false,
// The lists are stored as an object which has the list ids as keys.
lists: {},
}),
getters: {
getListById(state) {
return (id: IList['id']) => typeof state.lists[id] !== 'undefined' ? state.lists[id] : null
},
findListByExactname(state) {
return (name: string) => {
const list = Object.values(state.lists).find(l => {
return l.title.toLowerCase() === name.toLowerCase()
})
return typeof list === 'undefined' ? null : list
}
},
searchList(state) {
return (query: string, includeArchived = false) => {
return search(query)
?.filter(value => value > 0)
.map(id => state.lists[id])
.filter(list => list.isArchived === includeArchived)
|| []
}
},
},
actions: {
setIsLoading(isLoading: boolean) {
this.isLoading = isLoading
},
setList(list: IList) {
this.lists[list.id] = list
update(list)
},
setLists(lists: IList[]) {
lists.forEach(l => {
this.lists[l.id] = l
add(l)
})
},
removeListById(list: IList) {
remove(list)
delete this.lists[list.id]
},
toggleListFavorite(list: IList) {
// The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite
if (list.id === -1 || list.isArchived) {
return
}
return this.updateList({
...list,
isFavorite: !list.isFavorite,
})
},
async createList(list: IList) {
const cancel = setLoadingPinia(useListStore)
const listService = new ListService()
try {
const createdList = await listService.create(list)
createdList.namespaceId = list.namespaceId
vuexStore.commit('namespaces/addListToNamespace', createdList, {root: true})
this.setList(createdList)
return createdList
} finally {
cancel()
}
},
async updateList(list: IList) {
const cancel = setLoadingPinia(useListStore)
const listService = new ListService()
try {
await listService.update(list)
this.setList(list)
vuexStore.commit('namespaces/setListInNamespaceById', list, {root: true})
// the returned list from listService.update is the same!
// in order to not validate vuex mutations we have to create a new copy
const newList = {
...list,
namespaceId: FavoriteListsNamespace,
}
if (list.isFavorite) {
vuexStore.commit('namespaces/addListToNamespace', newList, {root: true})
} else {
vuexStore.commit('namespaces/removeListFromNamespaceById', newList, {root: true})
}
vuexStore.dispatch('namespaces/loadNamespacesIfFavoritesDontExist', null, {root: true})
vuexStore.dispatch('namespaces/removeFavoritesNamespaceIfEmpty', null, {root: true})
return newList
} catch (e) {
// Reset the list state to the initial one to avoid confusion for the user
this.setList({
...list,
isFavorite: !list.isFavorite,
})
throw e
} finally {
cancel()
}
},
async deleteList(list: IList) {
const cancel = setLoadingPinia(useListStore)
const listService = new ListService()
try {
const response = await listService.delete(list)
this.removeListById(list)
vuexStore.commit('namespaces/removeListFromNamespaceById', list, {root: true})
removeListFromHistory({id: list.id})
return response
} finally {
cancel()
}
},
},
})
export function useList(listId: MaybeRef<ListModel['id']>) {
const listService = shallowReactive(new ListService())
const {loading: isLoading} = toRefs(listService)
const list : ListModel = reactive(new ListModel({}))
const {t} = useI18n({useScope: 'global'})
watch(
() => unref(listId),
async (listId) => {
const loadedList = await listService.get(new ListModel({id: listId}))
Object.assign(list, loadedList)
},
{immediate: true},
)
const listStore = useListStore()
async function save() {
await listStore.updateList(list)
success({message: t('list.edit.success')})
}
return {
isLoading: readonly(isLoading),
list,
save,
}
}

View file

@ -71,10 +71,12 @@ import {getHistory} from '@/modules/listHistory'
import {parseDateOrNull} from '@/helpers/parseDateOrNull' import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate' import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate'
import {useDateTimeSalutation} from '@/composables/useDateTimeSalutation' import {useDateTimeSalutation} from '@/composables/useDateTimeSalutation'
import {useListStore} from '@/stores/lists'
const welcome = useDateTimeSalutation() const welcome = useDateTimeSalutation()
const store = useStore() const store = useStore()
const listStore = useListStore()
const listHistory = computed(() => { const listHistory = computed(() => {
// If we don't check this, it tries to load the list background right after logging out // If we don't check this, it tries to load the list background right after logging out
if(!store.state.auth.authenticated) { if(!store.state.auth.authenticated) {
@ -82,7 +84,7 @@ const listHistory = computed(() => {
} }
return getHistory() return getHistory()
.map(l => store.getters['lists/getListById'](l.id)) .map(l => listStore.getListById(l.id))
.filter(l => l !== null) .filter(l => l !== null)
}) })

View file

@ -15,11 +15,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import {computed} from 'vue' import {computed} from 'vue'
import {useStore} from '@/store'
import {setupMarkdownRenderer} from '@/helpers/markdownRenderer' import {setupMarkdownRenderer} from '@/helpers/markdownRenderer'
import {marked} from 'marked' import {marked} from 'marked'
import DOMPurify from 'dompurify' import DOMPurify from 'dompurify'
import {createRandomID} from '@/helpers/randomId' import {createRandomID} from '@/helpers/randomId'
import {useListStore} from '@/stores/lists'
const props = defineProps({ const props = defineProps({
listId: { listId: {
@ -28,8 +28,8 @@ const props = defineProps({
}, },
}) })
const store = useStore() const listStore = useListStore()
const list = computed(() => store.getters['lists/getListById'](props.listId)) const list = computed(() => listStore.getListById(props.listId))
const htmlDescription = computed(() => { const htmlDescription = computed(() => {
const description = list.value?.description || '' const description = list.value?.description || ''
if (description === '') { if (description === '') {

View file

@ -61,6 +61,7 @@ import {getListTitle} from '@/helpers/getListTitle'
import {saveListToHistory} from '@/modules/listHistory' import {saveListToHistory} from '@/modules/listHistory'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {useStore} from '@/store' import {useStore} from '@/store'
import {useListStore} from '@/stores/lists'
const props = defineProps({ const props = defineProps({
listId: { listId: {
@ -76,6 +77,7 @@ const props = defineProps({
const route = useRoute() const route = useRoute()
const store = useStore() const store = useStore()
const listStore = useListStore()
const listService = ref(new ListService()) const listService = ref(new ListService())
const loadedListId = ref(0) const loadedListId = ref(0)
@ -135,7 +137,7 @@ async function loadList(listIdToLoad: number) {
// Set the current list to the one we're about to load so that the title is already shown at the top // Set the current list to the one we're about to load so that the title is already shown at the top
loadedListId.value = 0 loadedListId.value = 0
const listFromStore = store.getters['lists/getListById'](listData.id) const listFromStore = listStore.getListById(listData.id)
if (listFromStore !== null) { if (listFromStore !== null) {
store.commit(BACKGROUND, null) store.commit(BACKGROUND, null)
store.commit(BLUR_HASH, null) store.commit(BLUR_HASH, null)

View file

@ -35,7 +35,6 @@
import {ref, reactive, shallowReactive} from 'vue' import {ref, reactive, shallowReactive} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {useRouter, useRoute} from 'vue-router' import {useRouter, useRoute} from 'vue-router'
import {useStore} from '@/store'
import ListService from '@/services/list' import ListService from '@/services/list'
import ListModel from '@/models/list' import ListModel from '@/models/list'
@ -44,9 +43,9 @@ import ColorPicker from '@/components/input/colorPicker.vue'
import {success} from '@/message' import {success} from '@/message'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {useListStore} from '@/stores/lists'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const store = useStore()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@ -55,6 +54,7 @@ useTitle(() => t('list.create.header'))
const showError = ref(false) const showError = ref(false)
const list = reactive(new ListModel()) const list = reactive(new ListModel())
const listService = shallowReactive(new ListService()) const listService = shallowReactive(new ListService())
const listStore = useListStore()
async function createNewList() { async function createNewList() {
if (list.title === '') { if (list.title === '') {
@ -64,7 +64,7 @@ async function createNewList() {
showError.value = false showError.value = false
list.namespaceId = Number(route.params.namespaceId as string) list.namespaceId = Number(route.params.namespaceId as string)
const newList = await store.dispatch('lists/createList', list) const newList = await listStore.createList(list)
await router.push({ await router.push({
name: 'list.index', name: 'list.index',
params: { listId: newList.id }, params: { listId: newList.id },

View file

@ -23,18 +23,20 @@ import {useI18n} from 'vue-i18n'
import { success } from '@/message' import { success } from '@/message'
import { useTitle } from '@/composables/useTitle' import { useTitle } from '@/composables/useTitle'
import { useListStore } from '@/stores/lists'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const store = useStore() const store = useStore()
const listStore = useListStore()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const list = computed(() => store.getters['lists/getListById'](route.params.listId)) const list = computed(() => listStore.getListById(route.params.listId))
useTitle(() => t('list.archive.title', {list: list.value.title})) useTitle(() => t('list.archive.title', {list: list.value.title}))
async function archiveList() { async function archiveList() {
try { try {
const newList = await store.dispatch('lists/updateList', { const newList = await listStore.updateList({
...list.value, ...list.value,
isArchived: !list.value.isArchived, isArchived: !list.value.isArchived,
}) })

View file

@ -105,6 +105,7 @@ import {useStore} from '@/store'
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import {useListStore} from '@/stores/lists'
import BackgroundUnsplashService from '@/services/backgroundUnsplash' import BackgroundUnsplashService from '@/services/backgroundUnsplash'
import BackgroundUploadService from '@/services/backgroundUpload' import BackgroundUploadService from '@/services/backgroundUpload'
@ -141,6 +142,7 @@ const debounceNewBackgroundSearch = debounce(newBackgroundSearch, SEARCH_DEBOUNC
const backgroundUploadService = ref(new BackgroundUploadService()) const backgroundUploadService = ref(new BackgroundUploadService())
const listService = ref(new ListService()) const listService = ref(new ListService())
const listStore = useListStore()
const unsplashBackgroundEnabled = computed(() => store.state.config.enabledBackgroundProviders.includes('unsplash')) const unsplashBackgroundEnabled = computed(() => store.state.config.enabledBackgroundProviders.includes('unsplash'))
const uploadBackgroundEnabled = computed(() => store.state.config.enabledBackgroundProviders.includes('upload')) const uploadBackgroundEnabled = computed(() => store.state.config.enabledBackgroundProviders.includes('upload'))
@ -176,6 +178,7 @@ async function searchBackgrounds(page = 1) {
}) })
} }
async function setBackground(backgroundId: string) { async function setBackground(backgroundId: string) {
// Don't set a background if we're in the process of setting one // Don't set a background if we're in the process of setting one
if (backgroundService.loading) { if (backgroundService.loading) {
@ -185,7 +188,7 @@ async function setBackground(backgroundId: string) {
const list = await backgroundService.update({id: backgroundId, listId: route.params.listId}) const list = await backgroundService.update({id: backgroundId, listId: route.params.listId})
await store.dispatch(CURRENT_LIST, {list, forceUpdate: true}) await store.dispatch(CURRENT_LIST, {list, forceUpdate: true})
store.commit('namespaces/setListInNamespaceById', list) store.commit('namespaces/setListInNamespaceById', list)
store.commit('lists/setList', list) listStore.setList(list)
success({message: t('list.background.success')}) success({message: t('list.background.success')})
} }
@ -198,7 +201,7 @@ async function uploadBackground() {
const list = await backgroundUploadService.value.create(route.params.listId, backgroundUploadInput.value?.files[0]) const list = await backgroundUploadService.value.create(route.params.listId, backgroundUploadInput.value?.files[0])
await store.dispatch(CURRENT_LIST, {list, forceUpdate: true}) await store.dispatch(CURRENT_LIST, {list, forceUpdate: true})
store.commit('namespaces/setListInNamespaceById', list) store.commit('namespaces/setListInNamespaceById', list)
store.commit('lists/setList', list) listStore.setList(list)
success({message: t('list.background.success')}) success({message: t('list.background.success')})
} }
@ -206,7 +209,7 @@ async function removeBackground() {
const list = await listService.value.removeBackground(currentList.value) const list = await listService.value.removeBackground(currentList.value)
await store.dispatch(CURRENT_LIST, {list, forceUpdate: true}) await store.dispatch(CURRENT_LIST, {list, forceUpdate: true})
store.commit('namespaces/setListInNamespaceById', list) store.commit('namespaces/setListInNamespaceById', list)
store.commit('lists/setList', list) listStore.setList(list)
success({message: t('list.background.removeSuccess')}) success({message: t('list.background.removeSuccess')})
router.back() router.back()
} }

View file

@ -30,24 +30,24 @@
import {computed, ref, watchEffect} from 'vue' import {computed, ref, watchEffect} from 'vue'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {useStore} from '@/store'
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
import {success} from '@/message' import {success} from '@/message'
import TaskCollectionService from '@/services/taskCollection' import TaskCollectionService from '@/services/taskCollection'
import Loading from '@/components/misc/loading.vue' import Loading from '@/components/misc/loading.vue'
import {useListStore} from '@/stores/lists'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const store = useStore() const listStore = useListStore()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const totalTasks = ref<number | null>(null) const totalTasks = ref<number | null>(null)
const list = computed(() => store.getters['lists/getListById'](route.params.listId)) const list = computed(() => listStore.getListById(route.params.listId))
watchEffect( watchEffect(
() => { () => {
if (!route.params.lisId) { if (!route.params.listId) {
return return
} }
@ -61,7 +61,11 @@ watchEffect(
useTitle(() => t('list.delete.title', {list: list?.value?.title})) useTitle(() => t('list.delete.title', {list: list?.value?.title}))
async function deleteList() { async function deleteList() {
await store.dispatch('lists/deleteList', list.value) if (!list.value) {
return
}
await listStore.deleteList(list.value)
success({message: t('list.delete.success')}) success({message: t('list.delete.success')})
router.push({name: 'home'}) router.push({name: 'home'})
} }

View file

@ -35,6 +35,7 @@ import type {INamespace} from '@/modelTypes/INamespace'
import {success} from '@/message' import {success} from '@/message'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {useNameSpaceSearch} from '@/composables/useNamespaceSearch' import {useNameSpaceSearch} from '@/composables/useNamespaceSearch'
import {useListStore} from '@/stores/lists'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
useTitle(() => t('list.duplicate.title')) useTitle(() => t('list.duplicate.title'))
@ -53,6 +54,7 @@ function selectNamespace(namespace: INamespace) {
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const store = useStore() const store = useStore()
const listStore = useListStore()
const listDuplicateService = shallowReactive(new ListDuplicateService()) const listDuplicateService = shallowReactive(new ListDuplicateService())
@ -66,7 +68,7 @@ async function duplicateList() {
const duplicate = await listDuplicateService.create(listDuplicate) const duplicate = await listDuplicateService.create(listDuplicate)
store.commit('namespaces/addListToNamespace', duplicate.list) store.commit('namespaces/addListToNamespace', duplicate.list)
store.commit('lists/setList', duplicate.list) listStore.setList(duplicate.list)
success({message: t('list.duplicate.success')}) success({message: t('list.duplicate.success')})
router.push({name: 'list.index', params: {listId: duplicate.list.id}}) router.push({name: 'list.index', params: {listId: duplicate.list.id}})
} }

View file

@ -45,7 +45,7 @@
<div class="field"> <div class="field">
<label class="label" for="listdescription">{{ $t('list.edit.description') }}</label> <label class="label" for="listdescription">{{ $t('list.edit.description') }}</label>
<div class="control"> <div class="control">
<editor <Editor
:class="{ 'disabled': isLoading}" :class="{ 'disabled': isLoading}"
:disabled="isLoading" :disabled="isLoading"
:previewIsDefault="false" :previewIsDefault="false"
@ -82,7 +82,7 @@ import CreateEdit from '@/components/misc/create-edit.vue'
import {CURRENT_LIST} from '@/store/mutation-types' import {CURRENT_LIST} from '@/store/mutation-types'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
import { useList } from '@/composables/useList' import {useList} from '@/stores/lists'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
const props = defineProps({ const props = defineProps({

View file

@ -148,6 +148,7 @@
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' import {defineComponent} from 'vue'
import { useListStore } from '@/stores/lists'
export default defineComponent({ export default defineComponent({
name: 'user-settings-general', name: 'user-settings-general',
@ -246,8 +247,9 @@ watch(
{immediate: true}, {immediate: true},
) )
const listStore = useListStore()
const defaultList = computed({ const defaultList = computed({
get: () => store.getters['lists/getListById'](settings.value.defaultListId), get: () => listStore.getListById(settings.value.defaultListId),
set(l) { set(l) {
settings.value.defaultListId = l ? l.id : DEFAULT_LIST_ID settings.value.defaultListId = l ? l.id : DEFAULT_LIST_ID
}, },