feat: singleTaskInList script setup (#2463)
Co-authored-by: Dominik Pschenitschni <mail@celement.de> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2463 Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de> Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
This commit is contained in:
parent
15b64c7e8a
commit
44e6981759
@ -61,8 +61,8 @@ const taskService = shallowReactive(new TaskService())
|
|||||||
const task = ref<ITask>()
|
const task = ref<ITask>()
|
||||||
|
|
||||||
// We're saving the due date seperately to prevent null errors in very short periods where the task is null.
|
// We're saving the due date seperately to prevent null errors in very short periods where the task is null.
|
||||||
const dueDate = ref<Date>()
|
const dueDate = ref<Date | null>()
|
||||||
const lastValue = ref<Date>()
|
const lastValue = ref<Date | null>()
|
||||||
const changeInterval = ref<ReturnType<typeof setInterval>>()
|
const changeInterval = ref<ReturnType<typeof setInterval>>()
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
@ -1,30 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="{'is-loading': taskService.loading}" class="task loader-container">
|
<div :class="{'is-loading': taskService.loading}" class="task loader-container">
|
||||||
<fancycheckbox :disabled="(isArchived || disabled) && !canMarkAsDone" @change="markAsDone" v-model="task.done"/>
|
<fancycheckbox
|
||||||
|
:disabled="(isArchived || disabled) && !canMarkAsDone"
|
||||||
|
@change="markAsDone"
|
||||||
|
v-model="task.done"
|
||||||
|
/>
|
||||||
|
|
||||||
<ColorBubble
|
<ColorBubble
|
||||||
v-if="showListColor && listColor !== ''"
|
v-if="showListColor && listColor !== ''"
|
||||||
:color="listColor"
|
:color="listColor"
|
||||||
class="mr-1"
|
class="mr-1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
:to="taskDetailRoute"
|
:to="taskDetailRoute"
|
||||||
:class="{ 'done': task.done}"
|
:class="{ 'done': task.done}"
|
||||||
class="tasktext">
|
class="tasktext"
|
||||||
|
>
|
||||||
<span>
|
<span>
|
||||||
<router-link
|
<router-link
|
||||||
|
v-if="showList && taskList !== null"
|
||||||
: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 && getListById(task.listId) !== null"
|
v-tooltip="$t('task.detail.belongsToList', {list: taskList.title})">
|
||||||
v-tooltip="$t('task.detail.belongsToList', {list: getListById(task.listId).title})">
|
{{ taskList.title }}
|
||||||
{{ getListById(task.listId).title }}
|
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<ColorBubble
|
<ColorBubble
|
||||||
v-if="task.hexColor !== ''"
|
v-if="task.hexColor !== ''"
|
||||||
:color="task.getHexColor()"
|
:color="getHexColor(task.hexColor)"
|
||||||
class="mr-1"
|
class="mr-1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Show any parent tasks to make it clear this task is a sub task of something -->
|
<!-- Show any parent tasks to make it clear this task is a sub task of something -->
|
||||||
<span class="parent-tasks" v-if="typeof task.relatedTasks.parenttask !== 'undefined'">
|
<span class="parent-tasks" v-if="typeof task.relatedTasks.parenttask !== 'undefined'">
|
||||||
<template v-for="(pt, i) in task.relatedTasks.parenttask">
|
<template v-for="(pt, i) in task.relatedTasks.parenttask">
|
||||||
@ -35,15 +43,22 @@
|
|||||||
{{ task.title }}
|
{{ task.title }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<labels class="labels ml-2 mr-1" :labels="task.labels" v-if="task.labels.length > 0" />
|
<labels
|
||||||
<user
|
v-if="task.labels.length > 0"
|
||||||
|
class="labels ml-2 mr-1"
|
||||||
|
:labels="task.labels"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<User
|
||||||
|
v-for="(a, i) in task.assignees"
|
||||||
:avatar-size="27"
|
:avatar-size="27"
|
||||||
:is-inline="true"
|
:is-inline="true"
|
||||||
:key="task.id + 'assignee' + a.id + i"
|
:key="task.id + 'assignee' + a.id + i"
|
||||||
:show-username="false"
|
:show-username="false"
|
||||||
:user="a"
|
:user="a"
|
||||||
v-for="(a, i) in task.assignees"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- FIXME: use popup -->
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="+new Date(task.dueDate) > 0"
|
v-if="+new Date(task.dueDate) > 0"
|
||||||
class="dueDate"
|
class="dueDate"
|
||||||
@ -62,7 +77,9 @@
|
|||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
|
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<priority-label :priority="task.priority" :done="task.done"/>
|
<priority-label :priority="task.priority" :done="task.done"/>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<span class="list-task-icon" v-if="task.attachments.length > 0">
|
<span class="list-task-icon" v-if="task.attachments.length > 0">
|
||||||
<icon icon="paperclip"/>
|
<icon icon="paperclip"/>
|
||||||
@ -74,74 +91,68 @@
|
|||||||
<icon icon="history"/>
|
<icon icon="history"/>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<checklist-summary :task="task"/>
|
<checklist-summary :task="task"/>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<progress
|
<progress
|
||||||
class="progress is-small"
|
class="progress is-small"
|
||||||
v-if="task.percentDone > 0"
|
v-if="task.percentDone > 0"
|
||||||
:value="task.percentDone * 100" max="100">
|
:value="task.percentDone * 100" max="100"
|
||||||
|
>
|
||||||
{{ task.percentDone * 100 }}%
|
{{ task.percentDone * 100 }}%
|
||||||
</progress>
|
</progress>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
|
v-if="!showList && currentList.id !== task.listId && taskList !== null"
|
||||||
: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 && getListById(task.listId) !== null"
|
v-tooltip="$t('task.detail.belongsToList', {list: taskList.title})"
|
||||||
v-tooltip="$t('task.detail.belongsToList', {list: getListById(task.listId).title})">
|
>
|
||||||
{{ getListById(task.listId).title }}
|
{{ taskList.title }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
:class="{'is-favorite': task.isFavorite}"
|
:class="{'is-favorite': task.isFavorite}"
|
||||||
@click="toggleFavorite"
|
@click="toggleFavorite"
|
||||||
class="favorite">
|
class="favorite"
|
||||||
|
>
|
||||||
<icon icon="star" v-if="task.isFavorite"/>
|
<icon icon="star" v-if="task.isFavorite"/>
|
||||||
<icon :icon="['far', 'star']" v-else/>
|
<icon :icon="['far', 'star']" v-else/>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
<slot></slot>
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {defineComponent, type PropType} from 'vue'
|
import {ref, watch, shallowReactive, toRef, type PropType, onMounted, onBeforeUnmount, computed} from 'vue'
|
||||||
import {mapState} from 'pinia'
|
import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
import TaskModel from '@/models/task'
|
import TaskModel, { getHexColor } from '@/models/task'
|
||||||
import type {ITask} from '@/modelTypes/ITask'
|
import type {ITask} from '@/modelTypes/ITask'
|
||||||
import PriorityLabel from './priorityLabel.vue'
|
|
||||||
import TaskService from '../../../services/task'
|
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
|
||||||
import Labels from '@/components/tasks/partials/labels.vue'
|
import Labels from '@/components/tasks/partials//labels.vue'
|
||||||
|
import DeferTask from '@/components/tasks/partials//defer-task.vue'
|
||||||
|
import ChecklistSummary from '@/components/tasks/partials/checklist-summary.vue'
|
||||||
|
|
||||||
import User from '@/components/misc/user.vue'
|
import User from '@/components/misc/user.vue'
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
import Fancycheckbox from '../../input/fancycheckbox.vue'
|
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||||
import DeferTask from './defer-task.vue'
|
|
||||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
|
||||||
import ChecklistSummary from './checklist-summary.vue'
|
|
||||||
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
|
|
||||||
import ColorBubble from '@/components/misc/colorBubble.vue'
|
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||||
|
|
||||||
|
import TaskService from '@/services/task'
|
||||||
|
|
||||||
|
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||||
|
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
|
||||||
|
import {success} from '@/message'
|
||||||
|
|
||||||
import {useListStore} from '@/stores/lists'
|
import {useListStore} from '@/stores/lists'
|
||||||
import {useNamespaceStore} from '@/stores/namespaces'
|
import {useNamespaceStore} from '@/stores/namespaces'
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
import {useTaskStore} from '@/stores/tasks'
|
import {useTaskStore} from '@/stores/tasks'
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps({
|
||||||
name: 'singleTaskInList',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
taskService: new TaskService(),
|
|
||||||
task: new TaskModel(),
|
|
||||||
showDefer: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
ColorBubble,
|
|
||||||
BaseButton,
|
|
||||||
ChecklistSummary,
|
|
||||||
DeferTask,
|
|
||||||
Fancycheckbox,
|
|
||||||
User,
|
|
||||||
Labels,
|
|
||||||
PriorityLabel,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
theTask: {
|
theTask: {
|
||||||
type: Object as PropType<ITask>,
|
type: Object as PropType<ITask>,
|
||||||
required: true,
|
required: true,
|
||||||
@ -166,61 +177,69 @@ export default defineComponent({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['task-updated'])
|
||||||
|
|
||||||
|
const {t} = useI18n({useScope: 'global'})
|
||||||
|
|
||||||
|
const taskService = shallowReactive(new TaskService())
|
||||||
|
const task = ref<ITask>(new TaskModel())
|
||||||
|
const showDefer = ref(false)
|
||||||
|
|
||||||
|
const theTask = toRef(props, 'theTask')
|
||||||
|
|
||||||
|
watch(
|
||||||
|
theTask,
|
||||||
|
newVal => {
|
||||||
|
task.value = newVal
|
||||||
},
|
},
|
||||||
emits: ['task-updated'],
|
)
|
||||||
watch: {
|
|
||||||
theTask(newVal) {
|
onMounted(() => {
|
||||||
this.task = newVal
|
task.value = theTask.value
|
||||||
},
|
document.addEventListener('click', hideDeferDueDatePopup)
|
||||||
},
|
})
|
||||||
mounted() {
|
|
||||||
this.task = this.theTask
|
onBeforeUnmount(() => {
|
||||||
document.addEventListener('click', this.hideDeferDueDatePopup)
|
document.removeEventListener('click', hideDeferDueDatePopup)
|
||||||
},
|
})
|
||||||
beforeUnmount() {
|
|
||||||
document.removeEventListener('click', this.hideDeferDueDatePopup)
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState(useListStore, {
|
|
||||||
getListById: 'getListById',
|
|
||||||
}),
|
|
||||||
listColor() {
|
|
||||||
const list = this.getListById(this.task.listId)
|
|
||||||
return list !== null ? list.hexColor : ''
|
|
||||||
},
|
|
||||||
currentList() {
|
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
|
const listStore = useListStore()
|
||||||
|
const taskStore = useTaskStore()
|
||||||
|
const namespaceStore = useNamespaceStore()
|
||||||
|
|
||||||
|
const taskList = computed(() => listStore.getListById(task.value.listId))
|
||||||
|
const listColor = computed(() => taskList.value !== null ? taskList.value.hexColor : '')
|
||||||
|
|
||||||
|
const currentList = computed(() => {
|
||||||
return typeof baseStore.currentList === 'undefined' ? {
|
return typeof baseStore.currentList === 'undefined' ? {
|
||||||
id: 0,
|
id: 0,
|
||||||
title: '',
|
title: '',
|
||||||
} : baseStore.currentList
|
} : baseStore.currentList
|
||||||
},
|
})
|
||||||
taskDetailRoute() {
|
|
||||||
return {
|
|
||||||
name: 'task.detail',
|
|
||||||
params: {id: this.task.id},
|
|
||||||
// TODO: re-enable opening task detail in modal
|
|
||||||
// state: { backdropView: this.$router.currentRoute.value.fullPath },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
formatDateSince,
|
|
||||||
formatISO,
|
|
||||||
formatDateLong,
|
|
||||||
|
|
||||||
async markAsDone(checked: boolean) {
|
const taskDetailRoute = computed(() => ({
|
||||||
|
name: 'task.detail',
|
||||||
|
params: {id: task.value.id},
|
||||||
|
// TODO: re-enable opening task detail in modal
|
||||||
|
// state: { backdropView: router.currentRoute.value.fullPath },
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
async function markAsDone(checked: boolean) {
|
||||||
const updateFunc = async () => {
|
const updateFunc = async () => {
|
||||||
const task = await useTaskStore().update(this.task)
|
const newTask = await taskStore.update(task.value)
|
||||||
this.task = task
|
task.value = newTask
|
||||||
this.$emit('task-updated', task)
|
emit('task-updated', newTask)
|
||||||
this.$message.success({
|
success({
|
||||||
message: this.task.done ?
|
message: task.value.done ?
|
||||||
this.$t('task.doneSuccess') :
|
t('task.doneSuccess') :
|
||||||
this.$t('task.undoneSuccess'),
|
t('task.undoneSuccess'),
|
||||||
}, [{
|
}, [{
|
||||||
title: 'Undo',
|
title: 'Undo',
|
||||||
callback: () => this.undoDone(checked),
|
callback: () => undoDone(checked),
|
||||||
}])
|
}])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,29 +248,29 @@ export default defineComponent({
|
|||||||
} else {
|
} else {
|
||||||
await updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
|
await updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
undoDone(checked: boolean) {
|
function undoDone(checked: boolean) {
|
||||||
this.task.done = !this.task.done
|
task.value.done = !task.value.done
|
||||||
this.markAsDone(!checked)
|
markAsDone(!checked)
|
||||||
},
|
}
|
||||||
|
|
||||||
async toggleFavorite() {
|
async function toggleFavorite() {
|
||||||
this.task.isFavorite = !this.task.isFavorite
|
task.value.isFavorite = !task.value.isFavorite
|
||||||
this.task = await this.taskService.update(this.task)
|
task.value = await taskService.update(task.value)
|
||||||
this.$emit('task-updated', this.task)
|
emit('task-updated', task.value)
|
||||||
useNamespaceStore().loadNamespacesIfFavoritesDontExist()
|
namespaceStore.loadNamespacesIfFavoritesDontExist()
|
||||||
},
|
}
|
||||||
hideDeferDueDatePopup(e) {
|
|
||||||
if (!this.showDefer) {
|
const deferDueDate = ref<typeof DeferTask | null>(null)
|
||||||
|
function hideDeferDueDatePopup(e) {
|
||||||
|
if (!showDefer.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
closeWhenClickedOutside(e, this.$refs.deferDueDate.$el, () => {
|
closeWhenClickedOutside(e, deferDueDate.value.$el, () => {
|
||||||
this.showDefer = false
|
showDefer.value = false
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user