feat: port tasks store to pina (#2409)

Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2409
This commit is contained in:
konrad 2022-09-30 11:17:19 +00:00
commit 8c394d8024
20 changed files with 158 additions and 131 deletions

View file

@ -73,6 +73,7 @@ import {PREFIXES} from '@/modules/parseTaskText'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
import {useLabelStore} from '@/stores/labels' import {useLabelStore} from '@/stores/labels'
import {useTaskStore} from '@/stores/tasks'
const TYPE_LIST = 'list' const TYPE_LIST = 'list'
const TYPE_TASK = 'task' const TYPE_TASK = 'task'
@ -412,7 +413,8 @@ export default defineComponent({
return return
} }
const task = await this.$store.dispatch('tasks/createNewTask', { const taskStore = useTaskStore()
const task = await taskStore.createNewTask({
title: this.query, title: this.query,
listId: this.currentList.id, listId: this.currentList.id,
}) })

View file

@ -43,12 +43,11 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, watch, unref, computed} from 'vue' import {ref, watch, unref, computed} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {useStore} from '@/store'
import {tryOnMounted, debouncedWatch, useWindowSize, type MaybeRef} from '@vueuse/core' import {tryOnMounted, debouncedWatch, useWindowSize, type MaybeRef} from '@vueuse/core'
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue' import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import {useTaskStore} from '@/stores/tasks'
function cleanupTitle(title: string) { function cleanupTitle(title: string) {
return title.replace(/^((\* |\+ |- )(\[ \] )?)/g, '') return title.replace(/^((\* |\+ |- )(\[ \] )?)/g, '')
@ -135,8 +134,8 @@ const newTaskTitle = ref('')
const newTaskInput = useAutoHeightTextarea(newTaskTitle) const newTaskInput = useAutoHeightTextarea(newTaskTitle)
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const store = useStore()
const authStore = useAuthStore() const authStore = useAuthStore()
const taskStore = useTaskStore()
const errorMessage = ref('') const errorMessage = ref('')
@ -149,7 +148,7 @@ function resetEmptyTitleError(e) {
} }
} }
const loading = computed(() => store.state[LOADING] && store.state[LOADING_MODULE] === 'tasks') const loading = computed(() => taskStore.isLoading)
async function addTask() { async function addTask() {
if (newTaskTitle.value === '') { if (newTaskTitle.value === '') {
errorMessage.value = t('list.create.addTitleRequired') errorMessage.value = t('list.create.addTitleRequired')
@ -168,7 +167,7 @@ async function addTask() {
return return
} }
const task = await store.dispatch('tasks/createNewTask', { const task = await taskStore.createNewTask({
title, title,
listId: authStore.settings.defaultListId, listId: authStore.settings.defaultListId,
position: props.defaultPosition, position: props.defaultPosition,

View file

@ -32,11 +32,12 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref,computed, watch, type PropType} from 'vue' import {ref,computed, watch, type PropType} from 'vue'
import {useStore} from '@/store'
import Editor from '@/components/input/AsyncEditor' import Editor from '@/components/input/AsyncEditor'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import {useTaskStore} from '@/stores/tasks'
import TaskModel from '@/models/task'
const props = defineProps({ const props = defineProps({
@ -55,14 +56,14 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const task = ref<ITask>({description: ''}) const task = ref<ITask>(new TaskModel())
const saved = ref(false) const saved = ref(false)
// Since loading is global state, this variable ensures we're only showing the saving icon when saving the description. // Since loading is global state, this variable ensures we're only showing the saving icon when saving the description.
const saving = ref(false) const saving = ref(false)
const store = useStore() const taskStore = useTaskStore()
const loading = computed(() => store.state.loading) const loading = computed(() => taskStore.isLoading)
watch( watch(
() => props.modelValue, () => props.modelValue,
@ -77,7 +78,7 @@ async function save() {
try { try {
// FIXME: don't update state from internal. // FIXME: don't update state from internal.
task.value = await store.dispatch('tasks/update', task.value) task.value = await taskStore.update(task.value)
emit('update:modelValue', task.value) emit('update:modelValue', task.value)
saved.value = true saved.value = true

View file

@ -29,7 +29,6 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, shallowReactive, watch, type PropType} from 'vue' import {ref, shallowReactive, watch, type PropType} from 'vue'
import {useStore} from '@/store'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import User from '@/components/misc/user.vue' import User from '@/components/misc/user.vue'
@ -39,6 +38,8 @@ import BaseButton from '@/components/base/BaseButton.vue'
import {includesById} from '@/helpers/utils' import {includesById} from '@/helpers/utils'
import ListUserService from '@/services/listUsers' import ListUserService from '@/services/listUsers'
import {success} from '@/message' import {success} from '@/message'
import {useTaskStore} from '@/stores/tasks'
import type {IUser} from '@/modelTypes/IUser' import type {IUser} from '@/modelTypes/IUser'
const props = defineProps({ const props = defineProps({
@ -60,7 +61,7 @@ const props = defineProps({
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const store = useStore() const taskStore = useTaskStore()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const listUserService = shallowReactive(new ListUserService()) const listUserService = shallowReactive(new ListUserService())
@ -79,13 +80,13 @@ watch(
) )
async function addAssignee(user: IUser) { async function addAssignee(user: IUser) {
await store.dispatch('tasks/addAssignee', {user: user, taskId: props.taskId}) await taskStore.addAssignee({user: user, taskId: props.taskId})
emit('update:modelValue', assignees.value) emit('update:modelValue', assignees.value)
success({message: t('task.assignee.assignSuccess')}) success({message: t('task.assignee.assignSuccess')})
} }
async function removeAssignee(user: IUser) { async function removeAssignee(user: IUser) {
await store.dispatch('tasks/removeAssignee', {user: user, taskId: props.taskId}) await taskStore.removeAssignee({user: user, taskId: props.taskId})
// Remove the assignee from the list // Remove the assignee from the list
for (const a in assignees.value) { for (const a in assignees.value) {

View file

@ -40,7 +40,6 @@
<script setup lang="ts"> <script setup lang="ts">
import {type PropType, ref, computed, shallowReactive, watch} from 'vue' import {type PropType, ref, computed, shallowReactive, watch} from 'vue'
import {useStore} from '@/store'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import LabelModel from '@/models/label' import LabelModel from '@/models/label'
@ -51,6 +50,7 @@ import BaseButton from '@/components/base/BaseButton.vue'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
import type {ILabel} from '@/modelTypes/ILabel' import type {ILabel} from '@/modelTypes/ILabel'
import {useLabelStore} from '@/stores/labels' import {useLabelStore} from '@/stores/labels'
import {useTaskStore} from '@/stores/tasks'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -69,7 +69,6 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const store = useStore()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const labelTaskService = shallowReactive(new LabelTaskService()) const labelTaskService = shallowReactive(new LabelTaskService())
@ -87,6 +86,7 @@ watch(
}, },
) )
const taskStore = useTaskStore()
const labelStore = useLabelStore() const labelStore = useLabelStore()
const foundLabels = computed(() => labelStore.filterLabelsByQuery(labels.value, query.value)) const foundLabels = computed(() => labelStore.filterLabelsByQuery(labels.value, query.value))
@ -97,17 +97,13 @@ function findLabel(newQuery: string) {
} }
async function addLabel(label: ILabel, showNotification = true) { async function addLabel(label: ILabel, showNotification = true) {
const bubble = () => {
emit('update:modelValue', labels.value)
}
if (props.taskId === 0) { if (props.taskId === 0) {
bubble() emit('update:modelValue', labels.value)
return return
} }
await store.dispatch('tasks/addLabel', {label, taskId: props.taskId}) await taskStore.addLabel({label, taskId: props.taskId})
bubble() emit('update:modelValue', labels.value)
if (showNotification) { if (showNotification) {
success({message: t('task.label.addSuccess')}) success({message: t('task.label.addSuccess')})
} }
@ -115,7 +111,7 @@ async function addLabel(label: ILabel, showNotification = true) {
async function removeLabel(label: ILabel) { async function removeLabel(label: ILabel) {
if (props.taskId !== 0) { if (props.taskId !== 0) {
await store.dispatch('tasks/removeLabel', {label, taskId: props.taskId}) await taskStore.removeLabel({label, taskId: props.taskId})
} }
for (const l in labels.value) { for (const l in labels.value) {

View file

@ -38,7 +38,6 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, type PropType} from 'vue' import {ref, computed, type PropType} from 'vue'
import {useStore} from '@/store'
import {useRouter} from 'vue-router' import {useRouter} from 'vue-router'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
@ -48,6 +47,7 @@ import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import ColorBubble from '@/components/misc/colorBubble.vue' import ColorBubble from '@/components/misc/colorBubble.vue'
import {useTaskStore} from '@/stores/tasks'
const props = defineProps({ const props = defineProps({
task: { task: {
@ -72,8 +72,8 @@ async function copyUrl() {
await copy(absoluteURL) await copy(absoluteURL)
} }
const store = useStore() const taskStore = useTaskStore()
const loading = computed(() => store.state.loading) const loading = computed(() => taskStore.isLoading)
const textIdentifier = computed(() => props.task?.getTextIdentifier() || '') const textIdentifier = computed(() => props.task?.getTextIdentifier() || '')
@ -93,7 +93,7 @@ async function save(title: string) {
try { try {
saving.value = true saving.value = true
const newTask = await store.dispatch('tasks/update', { const newTask = await taskStore.update({
...props.task, ...props.task,
title, title,
}) })

View file

@ -78,6 +78,7 @@ import type {ITask} from '@/modelTypes/ITask'
import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate' import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate'
import {colorIsDark} from '@/helpers/color/colorIsDark' import {colorIsDark} from '@/helpers/color/colorIsDark'
import {useTaskStore} from '@/stores/tasks'
export default defineComponent({ export default defineComponent({
name: 'kanban-card', name: 'kanban-card',
@ -121,7 +122,7 @@ export default defineComponent({
this.loadingInternal = true this.loadingInternal = true
try { try {
const done = !task.done const done = !task.done
await this.$store.dispatch('tasks/update', { await useTaskStore().update({
...task, ...task,
done, done,
}) })

View file

@ -149,7 +149,6 @@
import {ref, reactive, shallowReactive, watch, computed, type PropType} from 'vue' import {ref, reactive, shallowReactive, watch, computed, type PropType} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {useRoute} from 'vue-router' import {useRoute} from 'vue-router'
import {useStore} from '@/store'
import TaskService from '@/services/task' import TaskService from '@/services/task'
import TaskModel from '@/models/task' import TaskModel from '@/models/task'
@ -167,6 +166,7 @@ import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
import {error, success} from '@/message' import {error, success} from '@/message'
import {useTaskStore} from '@/stores/tasks'
const props = defineProps({ const props = defineProps({
taskId: { taskId: {
@ -190,7 +190,7 @@ const props = defineProps({
}, },
}) })
const store = useStore() const taskStore = useTaskStore()
const namespaceStore = useNamespaceStore() const namespaceStore = useNamespaceStore()
const route = useRoute() const route = useRoute()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
@ -344,7 +344,7 @@ async function createAndRelateTask(title: string) {
} }
async function toggleTaskDone(task: ITask) { async function toggleTaskDone(task: ITask) {
await store.dispatch('tasks/update', task) await taskStore.update(task)
// Find the task in the list and update it so that it is correctly strike through // Find the task in the list and update it so that it is correctly strike through
Object.entries(relatedTasks.value).some(([kind, tasks]) => { Object.entries(relatedTasks.value).some(([kind, tasks]) => {

View file

@ -119,6 +119,7 @@ import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatD
import ColorBubble from '@/components/misc/colorBubble.vue' import ColorBubble from '@/components/misc/colorBubble.vue'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
import {useTaskStore} from '@/stores/tasks'
export default defineComponent({ export default defineComponent({
name: 'singleTaskInList', name: 'singleTaskInList',
@ -208,7 +209,7 @@ export default defineComponent({
async markAsDone(checked: boolean) { async markAsDone(checked: boolean) {
const updateFunc = async () => { const updateFunc = async () => {
const task = await this.$store.dispatch('tasks/update', this.task) const task = await useTaskStore().update(this.task)
this.task = task this.task = task
this.$emit('task-updated', task) this.$emit('task-updated', task)
this.$message.success({ this.$message.success({

View file

@ -2,7 +2,7 @@ import AttachmentModel from '@/models/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment' import type {IAttachment} from '@/modelTypes/IAttachment'
import AttachmentService from '@/services/attachment' import AttachmentService from '@/services/attachment'
import { store } from '@/store' import {useTaskStore} from '@/stores/tasks'
export function uploadFile(taskId: number, file: File, onSuccess: (url: string) => void) { export function uploadFile(taskId: number, file: File, onSuccess: (url: string) => void) {
const attachmentService = new AttachmentService() const attachmentService = new AttachmentService()
@ -22,7 +22,7 @@ export async function uploadFiles(
console.debug(`Uploaded attachments for task ${taskId}, response was`, response) console.debug(`Uploaded attachments for task ${taskId}, response was`, response)
response.success?.map((attachment: IAttachment) => { response.success?.map((attachment: IAttachment) => {
store.dispatch('tasks/addTaskAttachment', { useTaskStore().addTaskAttachment({
taskId, taskId,
attachment, attachment,
}) })

View file

@ -1,8 +1,8 @@
import {createApp} from 'vue' import {createApp} from 'vue'
import App from './App.vue' import pinia from './pinia'
import router from './router' import router from './router'
import { createPinia } from 'pinia' import App from './App.vue'
import {error, success} from './message' import {error, success} from './message'
@ -105,7 +105,6 @@ if (window.SENTRY_ENABLED) {
import('./sentry').then(sentry => sentry.default(app, router)) import('./sentry').then(sentry => sentry.default(app, router))
} }
const pinia = createPinia()
app.use(pinia) app.use(pinia)
app.use(store, key) // pass the injection key app.use(store, key) // pass the injection key

5
src/pinia.ts Normal file
View file

@ -0,0 +1,5 @@
import {createPinia} from 'pinia'
const pinia = createPinia()
export default pinia

View file

@ -14,7 +14,6 @@ import {
QUICK_ACTIONS_ACTIVE, QUICK_ACTIONS_ACTIVE,
} from './mutation-types' } from './mutation-types'
import kanban from './modules/kanban' import kanban from './modules/kanban'
import tasks from './modules/tasks'
import ListModel from '@/models/list' import ListModel from '@/models/list'
@ -22,6 +21,7 @@ import ListService from '../services/list'
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl' import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
import type { RootStoreState, StoreState } from './types' import type { RootStoreState, StoreState } from './types'
import pinia from '@/pinia'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
export const key: InjectionKey<Store<StoreState>> = Symbol() export const key: InjectionKey<Store<StoreState>> = Symbol()
@ -35,7 +35,6 @@ export const store = createStore<RootStoreState>({
strict: import.meta.env.DEV, strict: import.meta.env.DEV,
modules: { modules: {
kanban, kanban,
tasks,
}, },
state: () => ({ state: () => ({
loading: false, loading: false,
@ -132,7 +131,7 @@ export const store = createStore<RootStoreState>({
}, },
async loadApp() { async loadApp() {
await checkAndSetApiUrl(window.API_URL) await checkAndSetApiUrl(window.API_URL)
const authStore = useAuthStore() const authStore = useAuthStore(pinia)
await authStore.checkAuth() await authStore.checkAuth()
}, },
}, },

View file

@ -94,7 +94,9 @@ export interface NamespaceState {
isLoading: boolean, isLoading: boolean,
} }
export interface TaskState {} export interface TaskState {
isLoading: boolean,
}
export type StoreState = RootStoreState & { export type StoreState = RootStoreState & {

View file

@ -124,7 +124,7 @@ export const useLabelStore = defineStore('label', {
const labelService = new LabelService() const labelService = new LabelService()
try { try {
const newLabel = await labelService.create(label) const newLabel = await labelService.create(label) as ILabel
this.setLabel(newLabel) this.setLabel(newLabel)
return newLabel return newLabel
} finally { } finally {

View file

@ -1,4 +1,4 @@
import type { Module } from 'vuex' import {defineStore, acceptHMRUpdate} from 'pinia'
import router from '@/router' import router from '@/router'
import {formatISO} from 'date-fns' import {formatISO} from 'date-fns'
@ -7,8 +7,8 @@ import TaskAssigneeService from '@/services/taskAssignee'
import LabelTaskService from '@/services/labelTask' import LabelTaskService from '@/services/labelTask'
import UserService from '@/services/user' import UserService from '@/services/user'
import {HAS_TASKS} from '../mutation-types' import {HAS_TASKS} from '../store/mutation-types'
import {setLoading} from '../helper' import {setLoadingPinia} from '../store/helper'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode' import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {parseTaskText} from '@/modules/parseTaskText' import {parseTaskText} from '@/modules/parseTaskText'
@ -24,11 +24,12 @@ import type { IUser } from '@/modelTypes/IUser'
import type {IAttachment} from '@/modelTypes/IAttachment' import type {IAttachment} from '@/modelTypes/IAttachment'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
import type { RootStoreState, TaskState } from '@/store/types' import type {TaskState} from '@/store/types'
import {useLabelStore} from '@/stores/labels' import {useLabelStore} from '@/stores/labels'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
import {useAttachmentStore} from '@/stores/attachments' import {useAttachmentStore} from '@/stores/attachments'
import {playPop} from '@/helpers/playPop' import {playPop} from '@/helpers/playPop'
import {store} from '@/store'
// 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) {
@ -43,7 +44,7 @@ function validateUsername(users: IUser[], username: IUser['username']) {
} }
// Check if the label exists // Check if the label exists
function validateLabel(labels: ILabel[], label: ILabel) { function validateLabel(labels: ILabel[], label: string) {
return findPropertyByValue(labels, 'title', label) return findPropertyByValue(labels, 'title', label)
} }
@ -58,7 +59,7 @@ async function addLabelToTask(task: ITask, label: ILabel) {
return response return response
} }
async function findAssignees(parsedTaskAssignees) { async function findAssignees(parsedTaskAssignees: string[]) {
if (parsedTaskAssignees.length <= 0) { if (parsedTaskAssignees.length <= 0) {
return [] return []
} }
@ -74,30 +75,31 @@ async function findAssignees(parsedTaskAssignees) {
} }
const tasksStore : Module<TaskState, RootStoreState>= { export const useTaskStore = defineStore('task', {
namespaced: true, state: () : TaskState => ({
state: () => ({}), isLoading: false,
}),
actions: { actions: {
async loadTasks(ctx, params) { async loadTasks(params) {
const taskService = new TaskService() const taskService = new TaskService()
const cancel = setLoading(ctx, 'tasks') const cancel = setLoadingPinia(this)
try { try {
const tasks = await taskService.getAll({}, params) const tasks = await taskService.getAll({}, params)
ctx.commit(HAS_TASKS, tasks.length > 0, {root: true}) store.commit(HAS_TASKS, tasks.length > 0)
return tasks return tasks
} finally { } finally {
cancel() cancel()
} }
}, },
async update(ctx, task: ITask) { async update(task: ITask) {
const cancel = setLoading(ctx, 'tasks') const cancel = setLoadingPinia(this)
const taskService = new TaskService() const taskService = new TaskService()
try { try {
const updatedTask = await taskService.update(task) const updatedTask = await taskService.update(task)
ctx.commit('kanban/setTaskInBucket', updatedTask, {root: true}) store.commit('kanban/setTaskInBucket', updatedTask)
if (task.done) { if (task.done) {
playPop() playPop()
} }
@ -107,23 +109,23 @@ const tasksStore : Module<TaskState, RootStoreState>= {
} }
}, },
async delete(ctx, task: ITask) { async delete(task: ITask) {
const taskService = new TaskService() const taskService = new TaskService()
const response = await taskService.delete(task) const response = await taskService.delete(task)
ctx.commit('kanban/removeTaskInBucket', task, {root: true}) store.commit('kanban/removeTaskInBucket', task)
return response return response
}, },
// Adds a task attachment in store. // Adds a task attachment in store.
// This is an action to be able to commit other mutations // This is an action to be able to commit other mutations
addTaskAttachment(ctx, { addTaskAttachment({
taskId, taskId,
attachment, attachment,
}: { }: {
taskId: ITask['id'] taskId: ITask['id']
attachment: IAttachment attachment: IAttachment
}) { }) {
const t = ctx.rootGetters['kanban/getTaskById'](taskId) const t = store.getters['kanban/getTaskById'](taskId)
if (t.task !== null) { if (t.task !== null) {
const attachments = [ const attachments = [
...t.task.attachments, ...t.task.attachments,
@ -137,24 +139,25 @@ const tasksStore : Module<TaskState, RootStoreState>= {
attachments, attachments,
}, },
} }
ctx.commit('kanban/setTaskInBucketByIndex', newTask, {root: true}) store.commit('kanban/setTaskInBucketByIndex', newTask)
} }
const attachmentStore = useAttachmentStore() const attachmentStore = useAttachmentStore()
attachmentStore.add(attachment) attachmentStore.add(attachment)
}, },
async addAssignee(ctx, { async addAssignee({
user, user,
taskId, taskId,
}: { }: {
user: IUser, user: IUser,
taskId: ITask['id'] taskId: ITask['id']
}) { }) {
const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: taskId})
const taskAssigneeService = new TaskAssigneeService() const taskAssigneeService = new TaskAssigneeService()
const r = await taskAssigneeService.create(taskAssignee) const r = await taskAssigneeService.create(new TaskAssigneeModel({
const t = ctx.rootGetters['kanban/getTaskById'](taskId) userId: user.id,
taskId: taskId,
}))
const t = store.getters['kanban/getTaskById'](taskId)
if (t.task === null) { if (t.task === null) {
// Don't try further adding a label if the task is not in kanban // Don't try further adding a label if the task is not in kanban
// Usually this means the kanban board hasn't been accessed until now. // Usually this means the kanban board hasn't been accessed until now.
@ -163,33 +166,32 @@ const tasksStore : Module<TaskState, RootStoreState>= {
return r return r
} }
const assignees = [ store.commit('kanban/setTaskInBucketByIndex', {
...t.task.assignees,
user,
]
ctx.commit('kanban/setTaskInBucketByIndex', {
...t, ...t,
task: { task: {
...t.task, ...t.task,
assignees, assignees: [
...t.task.assignees,
user,
],
}, },
}, {root: true}) })
return r return r
}, },
async removeAssignee(ctx, { async removeAssignee({
user, user,
taskId, taskId,
}: { }: {
user: IUser, user: IUser,
taskId: ITask['id'] taskId: ITask['id']
}) { }) {
const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: taskId})
const taskAssigneeService = new TaskAssigneeService() const taskAssigneeService = new TaskAssigneeService()
const response = await taskAssigneeService.delete(taskAssignee) const response = await taskAssigneeService.delete(new TaskAssigneeModel({
const t = ctx.rootGetters['kanban/getTaskById'](taskId) userId: user.id,
taskId: taskId,
}))
const t = store.getters['kanban/getTaskById'](taskId)
if (t.task === null) { if (t.task === null) {
// Don't try further adding a label if the task is not in kanban // Don't try further adding a label if the task is not in kanban
// Usually this means the kanban board hasn't been accessed until now. // Usually this means the kanban board hasn't been accessed until now.
@ -200,29 +202,30 @@ const tasksStore : Module<TaskState, RootStoreState>= {
const assignees = t.task.assignees.filter(({ id }) => id !== user.id) const assignees = t.task.assignees.filter(({ id }) => id !== user.id)
ctx.commit('kanban/setTaskInBucketByIndex', { store.commit('kanban/setTaskInBucketByIndex', {
...t, ...t,
task: { task: {
...t.task, ...t.task,
assignees, assignees,
}, },
}, {root: true}) })
return response return response
}, },
async addLabel(ctx, { async addLabel({
label, label,
taskId, taskId,
} : { } : {
label: ILabel, label: ILabel,
taskId: ITask['id'] taskId: ITask['id']
}) { }) {
const labelTask = new LabelTaskModel({taskId, labelId: label.id})
const labelTaskService = new LabelTaskService() const labelTaskService = new LabelTaskService()
const r = await labelTaskService.create(labelTask) const r = await labelTaskService.create(new LabelTaskModel({
const t = ctx.rootGetters['kanban/getTaskById'](taskId) taskId,
labelId: label.id,
}))
const t = store.getters['kanban/getTaskById'](taskId)
if (t.task === null) { if (t.task === null) {
// Don't try further adding a label if the task is not in kanban // Don't try further adding a label if the task is not in kanban
// Usually this means the kanban board hasn't been accessed until now. // Usually this means the kanban board hasn't been accessed until now.
@ -231,28 +234,30 @@ const tasksStore : Module<TaskState, RootStoreState>= {
return r return r
} }
const labels = [ store.commit('kanban/setTaskInBucketByIndex', {
...t.task.labels,
label,
]
ctx.commit('kanban/setTaskInBucketByIndex', {
...t, ...t,
task: { task: {
...t.task, ...t.task,
labels, labels: [
...t.task.labels,
label,
],
}, },
}, {root: true}) })
return r return r
}, },
async removeLabel(ctx, {label, taskId}) { async removeLabel(
const labelTask = new LabelTaskModel({taskId, labelId: label.id}) {label, taskId}:
{label: ILabel, taskId: ITask['id']},
) {
const labelTaskService = new LabelTaskService() const labelTaskService = new LabelTaskService()
const response = await labelTaskService.delete(labelTask) const response = await labelTaskService.delete(new LabelTaskModel({
const t = ctx.rootGetters['kanban/getTaskById'](taskId) taskId, labelId:
label.id,
}))
const t = store.getters['kanban/getTaskById'](taskId)
if (t.task === null) { if (t.task === null) {
// Don't try further adding a label if the task is not in kanban // Don't try further adding a label if the task is not in kanban
// Usually this means the kanban board hasn't been accessed until now. // Usually this means the kanban board hasn't been accessed until now.
@ -264,19 +269,22 @@ const tasksStore : Module<TaskState, RootStoreState>= {
// Remove the label from the list // Remove the label from the list
const labels = t.task.labels.filter(({ id }) => id !== label.id) const labels = t.task.labels.filter(({ id }) => id !== label.id)
ctx.commit('kanban/setTaskInBucketByIndex', { store.commit('kanban/setTaskInBucketByIndex', {
...t, ...t,
task: { task: {
...t.task, ...t.task,
labels, labels,
}, },
}, {root: true}) })
return response return response
}, },
// Do everything that is involved in finding, creating and adding the label to the task // Do everything that is involved in finding, creating and adding the label to the task
async addLabelsToTask(_, { task, parsedLabels }) { async addLabelsToTask(
{ task, parsedLabels }:
{ task: ITask, parsedLabels: string[] },
) {
if (parsedLabels.length <= 0) { if (parsedLabels.length <= 0) {
return task return task
} }
@ -299,10 +307,9 @@ const tasksStore : Module<TaskState, RootStoreState>= {
return task return task
}, },
findListId(_, { list: listName, listId }: { findListId(
list: string, { list: listName, listId }:
listId: IList['id'] { list: string, listId: IList['id'] }) {
}) {
let foundListId = null let foundListId = null
// 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:
@ -320,7 +327,7 @@ const tasksStore : Module<TaskState, RootStoreState>= {
// 3. Otherwise use the id from the route parameter // 3. Otherwise use the id from the route parameter
if (typeof router.currentRoute.value.params.listId !== 'undefined') { if (typeof router.currentRoute.value.params.listId !== 'undefined') {
foundListId = parseInt(router.currentRoute.value.params.listId) foundListId = Number(router.currentRoute.value.params.listId)
} }
// 4. If none of the above worked, reject the promise with an error. // 4. If none of the above worked, reject the promise with an error.
@ -331,7 +338,7 @@ const tasksStore : Module<TaskState, RootStoreState>= {
return foundListId return foundListId
}, },
async createNewTask(ctx, { async createNewTask({
title, title,
bucketId, bucketId,
listId, listId,
@ -339,10 +346,10 @@ const tasksStore : Module<TaskState, RootStoreState>= {
} : } :
Partial<ITask>, Partial<ITask>,
) { ) {
const cancel = setLoading(ctx, 'tasks') const cancel = setLoadingPinia(this)
const parsedTask = parseTaskText(title, getQuickAddMagicMode()) const parsedTask = parseTaskText(title, getQuickAddMagicMode())
const foundListId = await ctx.dispatch('findListId', { const foundListId = await this.findListId({
list: parsedTask.list, list: parsedTask.list,
listId: listId || 0, listId: listId || 0,
}) })
@ -369,7 +376,7 @@ const tasksStore : Module<TaskState, RootStoreState>= {
const taskService = new TaskService() const taskService = new TaskService()
const createdTask = await taskService.create(task) const createdTask = await taskService.create(task)
const result = await ctx.dispatch('addLabelsToTask', { const result = await this.addLabelsToTask({
task: createdTask, task: createdTask,
parsedLabels: parsedTask.labels, parsedLabels: parsedTask.labels,
}) })
@ -377,6 +384,9 @@ const tasksStore : Module<TaskState, RootStoreState>= {
return result return result
}, },
}, },
} })
export default tasksStore // support hot reloading
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useTaskStore, import.meta.hot))
}

View file

@ -240,6 +240,7 @@ import {calculateItemPosition} from '../../helpers/calculateItemPosition'
import KanbanCard from '@/components/tasks/partials/kanban-card.vue' import KanbanCard from '@/components/tasks/partials/kanban-card.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue' import DropdownItem from '@/components/misc/dropdown-item.vue'
import {isSavedFilter} from '@/helpers/savedFilter' import {isSavedFilter} from '@/helpers/savedFilter'
import {useTaskStore} from '@/stores/tasks'
const DRAG_OPTIONS = { const DRAG_OPTIONS = {
// sortable options // sortable options
@ -433,7 +434,8 @@ export default defineComponent({
) )
try { try {
await this.$store.dispatch('tasks/update', newTask) const taskStore = useTaskStore()
await taskStore.update(newTask)
// Make sure the first and second task don't both get position 0 assigned // Make sure the first and second task don't both get position 0 assigned
if(newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) { if(newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) {
@ -445,7 +447,7 @@ export default defineComponent({
taskAfterAfter !== null ? taskAfterAfter.kanbanPosition : null, taskAfterAfter !== null ? taskAfterAfter.kanbanPosition : null,
) )
await this.$store.dispatch('tasks/update', newTaskAfter) await taskStore.update(newTaskAfter)
} }
} finally { } finally {
this.taskUpdating[task.id] = false this.taskUpdating[task.id] = false
@ -464,7 +466,7 @@ export default defineComponent({
} }
this.newTaskError[bucketId] = false this.newTaskError[bucketId] = false
const task = await this.$store.dispatch('tasks/createNewTask', { const task = await useTaskStore().createNewTask({
title: this.newTaskText, title: this.newTaskText,
bucketId, bucketId,
listId: this.listId, listId: this.listId,

View file

@ -139,7 +139,7 @@ export default { name: 'List' }
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, toRef, nextTick, onMounted} from 'vue' import {ref, computed, toRef, nextTick, onMounted, type PropType} from 'vue'
import draggable from 'zhyswan-vuedraggable' import draggable from 'zhyswan-vuedraggable'
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
@ -161,6 +161,9 @@ import {RIGHTS as Rights} from '@/constants/rights'
import {calculateItemPosition} from '@/helpers/calculateItemPosition' import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import {isSavedFilter} from '@/helpers/savedFilter' import {isSavedFilter} from '@/helpers/savedFilter'
import {useTaskStore} from '@/stores/tasks'
import type {IList} from '@/modelTypes/IList'
function sortTasks(tasks: ITask[]) { function sortTasks(tasks: ITask[]) {
if (tasks === null || Array.isArray(tasks) && tasks.length === 0) { if (tasks === null || Array.isArray(tasks) && tasks.length === 0) {
@ -182,7 +185,7 @@ function sortTasks(tasks: ITask[]) {
const props = defineProps({ const props = defineProps({
listId: { listId: {
type: Number, type: Number as PropType<IList['id']>,
required: true, required: true,
}, },
}) })
@ -224,6 +227,7 @@ const firstNewPosition = computed(() => {
return calculateItemPosition(null, tasks.value[0].position) return calculateItemPosition(null, tasks.value[0].position)
}) })
const taskStore = useTaskStore()
const store = useStore() const store = useStore()
const list = computed(() => store.state.currentList) const list = computed(() => store.state.currentList)
@ -238,6 +242,7 @@ onMounted(async () => {
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
function searchTasks() { function searchTasks() {
// Only search if the search term changed // Only search if the search term changed
if (route.query as unknown as string === searchTerm.value) { if (route.query as unknown as string === searchTerm.value) {
@ -309,7 +314,7 @@ async function saveTaskPosition(e) {
position: calculateItemPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null), position: calculateItemPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null),
} }
const updatedTask = await this.$store.dispatch('tasks/update', newTask) const updatedTask = await taskStore.update(newTask)
tasks.value[e.newIndex] = updatedTask tasks.value[e.newIndex] = updatedTask
} }
</script> </script>

View file

@ -60,9 +60,11 @@ import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
import LlamaCool from '@/assets/llama-cool.svg?component' import LlamaCool from '@/assets/llama-cool.svg?component'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import {useTaskStore} from '@/stores/tasks'
const store = useStore() const store = useStore()
const authStore = useAuthStore() const authStore = useAuthStore()
const taskStore = useTaskStore()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
@ -180,7 +182,7 @@ async function loadPendingTasks(from: string, to: string) {
} }
} }
tasks.value = await store.dispatch('tasks/loadTasks', params) tasks.value = await taskStore.loadTasks(params)
} }
// FIXME: this modification should happen in the store // FIXME: this modification should happen in the store

View file

@ -464,6 +464,7 @@ import type {IList} from '@/modelTypes/IList'
import {colorIsDark} from '@/helpers/color/colorIsDark' import {colorIsDark} from '@/helpers/color/colorIsDark'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
import {useAttachmentStore} from '@/stores/attachments' import {useAttachmentStore} from '@/stores/attachments'
import {useTaskStore} from '@/stores/tasks'
function scrollIntoView(el) { function scrollIntoView(el) {
if (!el) { if (!el) {
@ -696,7 +697,8 @@ export default defineComponent({
task.endDate = task.dueDate task.endDate = task.dueDate
} }
this.task = await this.$store.dispatch('tasks/update', task)
this.task = await useTaskStore().update(task)
if (!showNotification) { if (!showNotification) {
return return
@ -728,7 +730,7 @@ export default defineComponent({
}, },
async deleteTask() { async deleteTask() {
await this.$store.dispatch('tasks/delete', this.task) await useTaskStore().delete(this.task)
this.$message.success({message: this.$t('task.detail.deleteSuccess')}) this.$message.success({message: this.$t('task.detail.deleteSuccess')})
this.$router.push({name: 'list.index', params: {listId: this.task.listId}}) this.$router.push({name: 'list.index', params: {listId: this.task.listId}})
}, },