wip: add useTask composable
This commit is contained in:
@@ -76,38 +76,32 @@
|
||||
v-if="projectStore.isLoading"
|
||||
variant="small"
|
||||
/>
|
||||
<template v-else>
|
||||
<nav
|
||||
v-if="favoriteProjects"
|
||||
class="menu"
|
||||
>
|
||||
<ProjectsNavigation
|
||||
:model-value="favoriteProjects"
|
||||
:can-edit-order="false"
|
||||
:can-collapse="false"
|
||||
/>
|
||||
</nav>
|
||||
|
||||
<nav
|
||||
v-if="savedFilterProjects"
|
||||
class="menu"
|
||||
>
|
||||
<ProjectsNavigation
|
||||
:model-value="savedFilterProjects"
|
||||
:can-edit-order="false"
|
||||
:can-collapse="false"
|
||||
/>
|
||||
</nav>
|
||||
<nav
|
||||
v-else
|
||||
class="menu"
|
||||
>
|
||||
<ProjectsNavigation
|
||||
v-if="favoriteProjects?.length"
|
||||
:model-value="favoriteProjects"
|
||||
:can-edit-order="false"
|
||||
:can-collapse="false"
|
||||
/>
|
||||
|
||||
<nav class="menu">
|
||||
<ProjectsNavigation
|
||||
:model-value="projects"
|
||||
:can-edit-order="true"
|
||||
:can-collapse="true"
|
||||
:level="1"
|
||||
/>
|
||||
</nav>
|
||||
</template>
|
||||
<ProjectsNavigation
|
||||
v-if="savedFilterProjects?.length"
|
||||
:model-value="savedFilterProjects"
|
||||
:can-edit-order="false"
|
||||
:can-collapse="false"
|
||||
/>
|
||||
|
||||
<ProjectsNavigation
|
||||
v-if="projects?.length"
|
||||
:model-value="projects"
|
||||
:can-edit-order="true"
|
||||
:can-collapse="true"
|
||||
:level="1"
|
||||
/>
|
||||
</nav>
|
||||
|
||||
<PoweredByLink
|
||||
class="mt-auto"
|
||||
@@ -117,7 +111,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import PoweredByLink from '@/components/home/PoweredByLink.vue'
|
||||
import Logo from '@/components/home/Logo.vue'
|
||||
@@ -129,16 +123,15 @@ import ProjectsNavigation from '@/components/home/ProjectsNavigation.vue'
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
const projects = computed(() => projectStore.notArchivedRootProjects)
|
||||
const favoriteProjects = computed(() => projectStore.favoriteProjects)
|
||||
const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
|
||||
const {
|
||||
notArchivedRootProjects: projects,
|
||||
favoriteProjects,
|
||||
savedFilterProjects,
|
||||
} = storeToRefs(projectStore)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.logo {
|
||||
display: block;
|
||||
|
||||
padding-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
@@ -192,7 +185,7 @@ const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
|
||||
}
|
||||
}
|
||||
|
||||
.menu + .menu {
|
||||
.menu-list + .menu-list {
|
||||
padding-top: math.div($navbar-padding, 2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -84,12 +84,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {storeToRefs} from 'pinia'
|
||||
import {useStorage} from '@vueuse/core'
|
||||
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import ProjectSettingsDropdown from '@/components/project/ProjectSettingsDropdown.vue'
|
||||
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||
@@ -105,18 +107,17 @@ const props = defineProps<{
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
const baseStore = useBaseStore()
|
||||
const currentProject = computed(() => baseStore.currentProject)
|
||||
const {currentProject} = storeToRefs(baseStore)
|
||||
|
||||
// Persist open state across browser reloads. Using a separate ref for the state
|
||||
// allows us to use only one entry in local storage instead of one for every project id.
|
||||
type OpenState = { [key: number]: boolean }
|
||||
const childProjectsOpenState = useStorage<OpenState>('navigation-child-projects-open', {})
|
||||
const childProjectsCollapsed = useStorage<{ [key: number]: boolean }>('navigation-child-projects-collapsed', {})
|
||||
const childProjectsOpen = computed({
|
||||
get() {
|
||||
return childProjectsOpenState.value[props.project.id] ?? true
|
||||
return childProjectsCollapsed.value[props.project.id] ?? true
|
||||
},
|
||||
set(open) {
|
||||
childProjectsOpenState.value[props.project.id] = open
|
||||
childProjectsCollapsed.value[props.project.id] = open
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -416,10 +416,7 @@ watch(
|
||||
collapsedBuckets.value = getCollapsedBucketState(projectId)
|
||||
kanbanStore.loadBucketsForProject(projectId, viewId, params)
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function setTaskContainerRef(id: IBucket['id'], el: HTMLElement) {
|
||||
|
||||
223
frontend/src/composables/useTask.ts
Normal file
223
frontend/src/composables/useTask.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { ref, reactive, computed, type MaybeRefOrGetter, toValue, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { klona } from 'klona/lite'
|
||||
|
||||
import type { ITask } from '@/modelTypes/ITask'
|
||||
import type { IProject } from '@/modelTypes/IProject'
|
||||
import type { Priority } from '@/constants/priorities'
|
||||
import type {Action as MessageAction} from '@/message'
|
||||
|
||||
import TaskModel from '@/models/task'
|
||||
|
||||
import { useTaskStore } from '@/stores/tasks'
|
||||
import { useProjectStore } from '@/stores/projects'
|
||||
import { useKanbanStore } from '@/stores/kanban'
|
||||
import { useBaseStore } from '@/stores/base'
|
||||
|
||||
import { RIGHTS } from '@/constants/rights'
|
||||
import { success } from '@/message'
|
||||
import { playPopSound } from '@/helpers/playPop'
|
||||
import { TASK_REPEAT_MODES } from '@/types/IRepeatMode'
|
||||
import { shallowReactive } from 'vue'
|
||||
import TaskService from '@/services/task'
|
||||
import { useAttachmentStore } from '@/stores/attachments'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { getProjectTitle } from '@/helpers/getProjectTitle'
|
||||
import { uploadFile } from '@/helpers/attachments'
|
||||
|
||||
export function useTask(taskId: MaybeRefOrGetter<ITask['id']>) {
|
||||
const {t} = useI18n()
|
||||
const router = useRouter()
|
||||
const taskStore = useTaskStore()
|
||||
|
||||
const taskTitle = ref('')
|
||||
|
||||
// We doubled the task color property here because verte does not have a real change property, leading
|
||||
// to the color property change being triggered when the # is removed from it, leading to an update,
|
||||
// which leads in turn to a change... This creates an infinite loop in which the task is updated, changed,
|
||||
// updated, changed, updated and so on.
|
||||
// To prevent this, we put the task color property in a seperate value which is set to the task color
|
||||
// when it is saved and loaded.
|
||||
const taskColor = ref<ITask['hexColor']>('')
|
||||
|
||||
const task = reactive({
|
||||
...new TaskModel(),
|
||||
title: taskTitle,
|
||||
}) as ITask
|
||||
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
|
||||
/** Avoid flashing of empty elements if the task content is not yet loaded. */
|
||||
const isReady = ref(false)
|
||||
|
||||
const isLoading = computed(() => taskService.loading)
|
||||
|
||||
// load task
|
||||
watch(
|
||||
() => toValue(taskId),
|
||||
async (newTaskId) => {
|
||||
if (newTaskId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const loaded = await taskService.get({ id: newTaskId })
|
||||
Object.assign(task, loaded)
|
||||
attachmentStore.set(task.attachments)
|
||||
taskColor.value = task.hexColor
|
||||
setActiveFields()
|
||||
} finally {
|
||||
isReady.value = true
|
||||
}
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const canWrite = computed(() => (
|
||||
task.maxRight !== null &&
|
||||
task.maxRight > RIGHTS.READ
|
||||
))
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
const project = computed(() => projectStore.projects[task.projectId])
|
||||
|
||||
const ancestorProjects = computed(() => projectStore.getAncestors(project.value))
|
||||
|
||||
const ancestorProjectTitles = computed(() => ancestorProjects.value.map(project => getProjectTitle(project)))
|
||||
|
||||
const attachmentStore = useAttachmentStore()
|
||||
const {hasAttachments} = storeToRefs(attachmentStore)
|
||||
|
||||
async function saveTask(
|
||||
currentTask: ITask | null = null,
|
||||
undoCallback?: () => void,
|
||||
) {
|
||||
if (currentTask === null) {
|
||||
currentTask = klona(task)
|
||||
}
|
||||
|
||||
if (!canWrite.value) {
|
||||
return
|
||||
}
|
||||
|
||||
currentTask.hexColor = taskColor.value
|
||||
|
||||
// If no end date is being set, but a start date and due date,
|
||||
// use the due date as the end date
|
||||
if (
|
||||
currentTask.endDate === null &&
|
||||
currentTask.startDate !== null &&
|
||||
currentTask.dueDate !== null
|
||||
) {
|
||||
currentTask.endDate = currentTask.dueDate
|
||||
}
|
||||
|
||||
const updatedTask = await taskStore.update(currentTask) // TODO: markraw ?
|
||||
Object.assign(task, updatedTask)
|
||||
setActiveFields()
|
||||
|
||||
let actions: MessageAction[] = []
|
||||
if (undoCallback) {
|
||||
actions = [{
|
||||
title: t('task.undo'),
|
||||
callback: undoCallback,
|
||||
}]
|
||||
}
|
||||
success({message: t('task.detail.updateSuccess')}, actions)
|
||||
}
|
||||
|
||||
async function deleteTask() {
|
||||
await taskStore.delete(task)
|
||||
success({message: t('task.detail.deleteSuccess')})
|
||||
return router.push({name: 'project.index', params: {projectId: task.projectId}})
|
||||
}
|
||||
|
||||
function uploadAttachment(file: File, onSuccess?: (url: string) => void) {
|
||||
return uploadFile(toValue(taskId), file, onSuccess)
|
||||
}
|
||||
|
||||
async function toggleTaskDone() {
|
||||
const newTask = {
|
||||
...klona(task),
|
||||
done: !task.done,
|
||||
}
|
||||
|
||||
if (newTask.done) {
|
||||
playPopSound()
|
||||
}
|
||||
|
||||
await saveTask(
|
||||
newTask,
|
||||
toggleTaskDone,
|
||||
)
|
||||
}
|
||||
|
||||
async function changeProject(project: IProject) {
|
||||
const kanbanStore = useKanbanStore()
|
||||
const baseStore = useBaseStore()
|
||||
kanbanStore.removeTaskInBucket(task)
|
||||
await saveTask({
|
||||
...task,
|
||||
projectId: project.id,
|
||||
})
|
||||
baseStore.setCurrentProject(project)
|
||||
}
|
||||
|
||||
async function toggleFavorite() {
|
||||
const newTask = await taskStore.toggleFavorite(task)
|
||||
Object.assign(task, newTask)
|
||||
}
|
||||
|
||||
async function setPriority(priority: Priority) {
|
||||
const newTask: ITask = {
|
||||
...task,
|
||||
priority,
|
||||
}
|
||||
|
||||
return saveTask(newTask)
|
||||
}
|
||||
|
||||
async function setPercentDone(percentDone: number) {
|
||||
const newTask: ITask = {
|
||||
...task,
|
||||
percentDone,
|
||||
}
|
||||
|
||||
return saveTask(newTask)
|
||||
}
|
||||
|
||||
async function removeRepeatAfter() {
|
||||
task.repeatAfter.amount = 0
|
||||
task.repeatMode = TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
|
||||
await saveTask()
|
||||
}
|
||||
|
||||
return {
|
||||
project,
|
||||
ancestorProjects,
|
||||
ancestorProjectTitles,
|
||||
|
||||
isReady,
|
||||
isLoading,
|
||||
|
||||
task,
|
||||
taskTitle,
|
||||
taskColor,
|
||||
|
||||
canWrite,
|
||||
hasAttachments,
|
||||
|
||||
saveTask,
|
||||
deleteTask,
|
||||
uploadAttachment,
|
||||
|
||||
toggleTaskDone,
|
||||
changeProject,
|
||||
toggleFavorite,
|
||||
setPriority,
|
||||
setPercentDone,
|
||||
removeRepeatAfter,
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watchEffect} from 'vue'
|
||||
import {computed, shallowReactive, watchEffect} from 'vue'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
@@ -46,36 +46,45 @@ const projectStore = useProjectStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const totalTasks = ref<number | null>(null)
|
||||
const projectId = computed(() => Number(route.params.projectId))
|
||||
|
||||
const project = computed(() => projectStore.projects[route.params.projectId])
|
||||
const projectIdsToDelete = ref<number[]>([])
|
||||
const project = computed(() => projectStore.projects[projectId.value])
|
||||
|
||||
const projectIdsToDelete = computed(() => {
|
||||
if (!projectId.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
...projectStore
|
||||
.getChildProjects(projectId.value)
|
||||
.map(p => p.id),
|
||||
projectId.value,
|
||||
]
|
||||
})
|
||||
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
watchEffect(
|
||||
async () => {
|
||||
if (!route.params.projectId) {
|
||||
if (!projectIdsToDelete.value.length) {
|
||||
return
|
||||
}
|
||||
|
||||
projectIdsToDelete.value = projectStore
|
||||
.getChildProjects(parseInt(route.params.projectId))
|
||||
.map(p => p.id)
|
||||
|
||||
projectIdsToDelete.value.push(parseInt(route.params.projectId))
|
||||
|
||||
const taskService = new TaskService()
|
||||
await taskService.getAll({}, {filter: `project in ${projectIdsToDelete.value.join(',')}`})
|
||||
totalTasks.value = taskService.totalPages * taskService.resultCount
|
||||
},
|
||||
)
|
||||
const totalTasks = computed(() => taskService.totalPages * taskService.resultCount)
|
||||
|
||||
useTitle(() => t('project.delete.title', {project: project?.value?.title}))
|
||||
|
||||
const deleteNotice = computed(() => {
|
||||
if(totalTasks.value && totalTasks.value > 0) {
|
||||
if (totalTasks.value && totalTasks.value > 0) {
|
||||
if (projectIdsToDelete.value.length <= 1) {
|
||||
return t('project.delete.tasksToDelete', {count: totalTasks.value})
|
||||
} else if (projectIdsToDelete.value.length > 1) {
|
||||
}
|
||||
|
||||
if (projectIdsToDelete.value.length > 1) {
|
||||
return t('project.delete.tasksAndChildProjectsToDelete', {tasks: totalTasks.value, projects: projectIdsToDelete.value.length})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
class="loader-container task-view-container"
|
||||
:class="{
|
||||
'is-loading': taskService.loading || !isReady,
|
||||
'is-loading': isLoading || !isReady,
|
||||
'is-modal': isModal,
|
||||
}"
|
||||
>
|
||||
@@ -24,7 +24,7 @@
|
||||
class="subtitle"
|
||||
>
|
||||
<template
|
||||
v-for="p in projectStore.getAncestors(project)"
|
||||
v-for="p in ancestorProjects"
|
||||
:key="p.id"
|
||||
>
|
||||
<a
|
||||
@@ -117,7 +117,7 @@
|
||||
:ref="e => setFieldRef('dueDate', e)"
|
||||
v-model="task.dueDate"
|
||||
:choose-date-label="$t('task.detail.chooseDueDate')"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
:disabled="isLoading || !canWrite"
|
||||
@closeOnChange="saveTask()"
|
||||
/>
|
||||
<BaseButton
|
||||
@@ -171,7 +171,7 @@
|
||||
:ref="e => setFieldRef('startDate', e)"
|
||||
v-model="task.startDate"
|
||||
:choose-date-label="$t('task.detail.chooseStartDate')"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
:disabled="isLoading || !canWrite"
|
||||
@closeOnChange="saveTask()"
|
||||
/>
|
||||
<BaseButton
|
||||
@@ -204,7 +204,7 @@
|
||||
:ref="e => setFieldRef('endDate', e)"
|
||||
v-model="task.endDate"
|
||||
:choose-date-label="$t('task.detail.chooseEndDate')"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
:disabled="isLoading || !canWrite"
|
||||
@closeOnChange="saveTask()"
|
||||
/>
|
||||
<BaseButton
|
||||
@@ -320,7 +320,7 @@
|
||||
<Description
|
||||
:model-value="task"
|
||||
:can-write="canWrite"
|
||||
:attachment-upload="attachmentUpload"
|
||||
:attachment-upload="uploadAttachment"
|
||||
@update:modelValue="Object.assign(task, $event)"
|
||||
/>
|
||||
</div>
|
||||
@@ -415,10 +415,9 @@
|
||||
{{ task.done ? $t('task.detail.undone') : $t('task.detail.done') }}
|
||||
</x-button>
|
||||
<TaskSubscription
|
||||
v-model="task.subscription"
|
||||
entity="task"
|
||||
:entity-id="task.id"
|
||||
:model-value="task.subscription"
|
||||
@update:modelValue="sub => task.subscription = sub"
|
||||
/>
|
||||
<x-button
|
||||
v-shortcut="'s'"
|
||||
@@ -585,22 +584,14 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, reactive, shallowReactive, computed, watch, nextTick, onMounted, onBeforeUnmount} from 'vue'
|
||||
import {ref, reactive, computed, nextTick, watchPostEffect, watch} from 'vue'
|
||||
import {useRouter, type RouteLocation} from 'vue-router'
|
||||
import {storeToRefs} from 'pinia'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {unrefElement} from '@vueuse/core'
|
||||
import {klona} from 'klona/lite'
|
||||
import {unrefElement, useEventListener} from '@vueuse/core'
|
||||
import {eventToHotkeyString} from '@github/hotkey'
|
||||
|
||||
import TaskService from '@/services/task'
|
||||
import TaskModel from '@/models/task'
|
||||
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import {PRIORITIES, type Priority} from '@/constants/priorities'
|
||||
import {RIGHTS} from '@/constants/rights'
|
||||
import {PRIORITIES} from '@/constants/priorities'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
@@ -626,23 +617,14 @@ import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import AssigneeList from '@/components/tasks/partials/AssigneeList.vue'
|
||||
import Reactions from '@/components/input/Reactions.vue'
|
||||
|
||||
import {uploadFile} from '@/helpers/attachments'
|
||||
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||
import {scrollIntoView} from '@/helpers/scrollIntoView'
|
||||
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
|
||||
import {playPopSound} from '@/helpers/playPop'
|
||||
|
||||
import {useAttachmentStore} from '@/stores/attachments'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import {useKanbanStore} from '@/stores/kanban'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
||||
import {success} from '@/message'
|
||||
import type {Action as MessageAction} from '@/message'
|
||||
import {useTask} from '@/composables/useTask'
|
||||
|
||||
const props = defineProps<{
|
||||
taskId: ITask['id'],
|
||||
@@ -654,98 +636,59 @@ defineEmits<{
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
const attachmentStore = useAttachmentStore()
|
||||
const {hasAttachments} = storeToRefs(attachmentStore)
|
||||
const taskStore = useTaskStore()
|
||||
const kanbanStore = useKanbanStore()
|
||||
const authStore = useAuthStore()
|
||||
const baseStore = useBaseStore()
|
||||
|
||||
const task = ref<ITask>(new TaskModel())
|
||||
const taskTitle = computed(() => task.value.title)
|
||||
const {
|
||||
project,
|
||||
ancestorProjects,
|
||||
|
||||
isReady,
|
||||
isLoading,
|
||||
|
||||
task,
|
||||
taskTitle,
|
||||
taskColor,
|
||||
|
||||
canWrite,
|
||||
hasAttachments,
|
||||
|
||||
saveTask,
|
||||
deleteTask,
|
||||
uploadAttachment,
|
||||
|
||||
toggleTaskDone,
|
||||
changeProject,
|
||||
toggleFavorite,
|
||||
setPriority,
|
||||
setPercentDone,
|
||||
removeRepeatAfter,
|
||||
} = useTask(() => props.taskId)
|
||||
|
||||
useTitle(taskTitle)
|
||||
|
||||
|
||||
// See https://github.com/github/hotkey/discussions/85#discussioncomment-5214660
|
||||
function saveTaskViaHotkey(event) {
|
||||
useEventListener('keydown', (event) => {
|
||||
const hotkeyString = eventToHotkeyString(event)
|
||||
if (!hotkeyString) return
|
||||
if (hotkeyString !== 'Control+s' && hotkeyString !== 'Meta+s') return
|
||||
event.preventDefault()
|
||||
|
||||
saveTask()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', saveTaskViaHotkey)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('keydown', saveTaskViaHotkey)
|
||||
})
|
||||
|
||||
// We doubled the task color property here because verte does not have a real change property, leading
|
||||
// to the color property change being triggered when the # is removed from it, leading to an update,
|
||||
// which leads in turn to a change... This creates an infinite loop in which the task is updated, changed,
|
||||
// updated, changed, updated and so on.
|
||||
// To prevent this, we put the task color property in a seperate value which is set to the task color
|
||||
// when it is saved and loaded.
|
||||
const taskColor = ref<ITask['hexColor']>('')
|
||||
|
||||
// Used to avoid flashing of empty elements if the task content is not yet loaded.
|
||||
const isReady = ref(false)
|
||||
|
||||
const project = computed(() => projectStore.projects[task.value.projectId])
|
||||
|
||||
const canWrite = computed(() => (
|
||||
task.value.maxRight !== null &&
|
||||
task.value.maxRight > RIGHTS.READ
|
||||
))
|
||||
|
||||
const color = computed(() => {
|
||||
const color = task.value.getHexColor
|
||||
? task.value.getHexColor()
|
||||
: undefined
|
||||
|
||||
return color
|
||||
})
|
||||
const color = computed(() => task?.getHexColor())
|
||||
|
||||
const isModal = computed(() => Boolean(props.backdropView))
|
||||
|
||||
function attachmentUpload(file: File, onSuccess?: (url: string) => void) {
|
||||
return uploadFile(props.taskId, file, onSuccess)
|
||||
}
|
||||
|
||||
const heading = ref<HTMLElement | null>(null)
|
||||
|
||||
async function scrollToHeading() {
|
||||
scrollIntoView(unrefElement(heading))
|
||||
}
|
||||
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
|
||||
// load task
|
||||
watch(
|
||||
() => props.taskId,
|
||||
async (id) => {
|
||||
if (id === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const loaded = await taskService.get({id})
|
||||
Object.assign(task.value, loaded)
|
||||
attachmentStore.set(task.value.attachments)
|
||||
taskColor.value = task.value.hexColor
|
||||
setActiveFields()
|
||||
} finally {
|
||||
await nextTick()
|
||||
scrollToHeading()
|
||||
isReady.value = true
|
||||
}
|
||||
}, {immediate: true})
|
||||
watchPostEffect(() => {
|
||||
if (isReady.value) {
|
||||
scrollIntoView(unrefElement(heading))
|
||||
}
|
||||
})
|
||||
|
||||
type FieldType =
|
||||
| 'assignees'
|
||||
@@ -784,17 +727,19 @@ function setActiveFields() {
|
||||
// task.endDate = task.endDate || null
|
||||
|
||||
// Set all active fields based on values in the model
|
||||
activeFields.assignees = task.value.assignees.length > 0
|
||||
activeFields.attachments = task.value.attachments.length > 0
|
||||
activeFields.dueDate = task.value.dueDate !== null
|
||||
activeFields.endDate = task.value.endDate !== null
|
||||
activeFields.labels = task.value.labels.length > 0
|
||||
activeFields.percentDone = task.value.percentDone > 0
|
||||
activeFields.priority = task.value.priority !== PRIORITIES.UNSET
|
||||
activeFields.relatedTasks = Object.keys(task.value.relatedTasks).length > 0
|
||||
activeFields.reminders = task.value.reminders.length > 0
|
||||
activeFields.repeatAfter = task.value.repeatAfter?.amount > 0 || task.value.repeatMode !== TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
|
||||
activeFields.startDate = task.value.startDate !== null
|
||||
Object.assign(activeFields, {
|
||||
assignees: task.assignees.length > 0,
|
||||
attachments: task.attachments.length > 0,
|
||||
dueDate: task.dueDate !== null,
|
||||
endDate: task.endDate !== null,
|
||||
labels: task.labels.length > 0,
|
||||
percentDone: task.percentDone > 0,
|
||||
priority: task.priority !== PRIORITIES.UNSET,
|
||||
relatedTasks: Object.keys(task.relatedTasks).length > 0,
|
||||
reminders: task.reminders.length > 0,
|
||||
repeatAfter: task.repeatAfter?.amount > 0 || task.repeatMode !== TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT,
|
||||
startDate: task.startDate !== null,
|
||||
})
|
||||
}
|
||||
|
||||
const activeFieldElements: { [id in FieldType]: HTMLElement | null } = reactive({
|
||||
@@ -833,106 +778,8 @@ function setFieldActive(fieldName: keyof typeof activeFields) {
|
||||
})
|
||||
}
|
||||
|
||||
async function saveTask(
|
||||
currentTask: ITask | null = null,
|
||||
undoCallback?: () => void,
|
||||
) {
|
||||
if (currentTask === null) {
|
||||
currentTask = klona(task.value)
|
||||
}
|
||||
|
||||
if (!canWrite.value) {
|
||||
return
|
||||
}
|
||||
|
||||
currentTask.hexColor = taskColor.value
|
||||
|
||||
// If no end date is being set, but a start date and due date,
|
||||
// use the due date as the end date
|
||||
if (
|
||||
currentTask.endDate === null &&
|
||||
currentTask.startDate !== null &&
|
||||
currentTask.dueDate !== null
|
||||
) {
|
||||
currentTask.endDate = currentTask.dueDate
|
||||
}
|
||||
|
||||
const updatedTask = await taskStore.update(currentTask) // TODO: markraw ?
|
||||
Object.assign(task.value, updatedTask)
|
||||
setActiveFields()
|
||||
|
||||
let actions: MessageAction[] = []
|
||||
if (undoCallback) {
|
||||
actions = [{
|
||||
title: t('task.undo'),
|
||||
callback: undoCallback,
|
||||
}]
|
||||
}
|
||||
success({message: t('task.detail.updateSuccess')}, actions)
|
||||
}
|
||||
|
||||
const showDeleteModal = ref(false)
|
||||
|
||||
async function deleteTask() {
|
||||
await taskStore.delete(task.value)
|
||||
success({message: t('task.detail.deleteSuccess')})
|
||||
router.push({name: 'project.index', params: {projectId: task.value.projectId}})
|
||||
}
|
||||
|
||||
async function toggleTaskDone() {
|
||||
const newTask = {
|
||||
...task.value,
|
||||
done: !task.value.done,
|
||||
}
|
||||
|
||||
if (newTask.done) {
|
||||
playPopSound()
|
||||
}
|
||||
|
||||
await saveTask(
|
||||
newTask,
|
||||
toggleTaskDone,
|
||||
)
|
||||
}
|
||||
|
||||
async function changeProject(project: IProject) {
|
||||
kanbanStore.removeTaskInBucket(task.value)
|
||||
await saveTask({
|
||||
...task.value,
|
||||
projectId: project.id,
|
||||
})
|
||||
baseStore.setCurrentProject(project)
|
||||
}
|
||||
|
||||
async function toggleFavorite() {
|
||||
const newTask = await taskStore.toggleFavorite(task.value)
|
||||
Object.assign(task.value, newTask)
|
||||
}
|
||||
|
||||
async function setPriority(priority: Priority) {
|
||||
const newTask: ITask = {
|
||||
...task.value,
|
||||
priority,
|
||||
}
|
||||
|
||||
return saveTask(newTask)
|
||||
}
|
||||
|
||||
async function setPercentDone(percentDone: number) {
|
||||
const newTask: ITask = {
|
||||
...task.value,
|
||||
percentDone,
|
||||
}
|
||||
|
||||
return saveTask(newTask)
|
||||
}
|
||||
|
||||
async function removeRepeatAfter() {
|
||||
task.value.repeatAfter.amount = 0
|
||||
task.value.repeatMode = TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
|
||||
await saveTask()
|
||||
}
|
||||
|
||||
function setRelatedTasksActive() {
|
||||
setFieldActive('relatedTasks')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user