feat: singleTaskInList script setup #2463

Merged
konrad merged 1 commits from dpschen/frontend:feature/feat-singleTaskInList-script-setup into main 2022-11-01 10:43:03 +00:00
2 changed files with 171 additions and 152 deletions

View File

@ -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(

View File

@ -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,184 +91,186 @@
<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', theTask: {
data() { type: Object as PropType<ITask>,
return { required: true,
taskService: new TaskService(),
task: new TaskModel(),
showDefer: false,
}
}, },
components: { isArchived: {
ColorBubble, type: Boolean,
BaseButton, default: false,
ChecklistSummary,
DeferTask,
Fancycheckbox,
User,
Labels,
PriorityLabel,
}, },
props: { showList: {
theTask: { type: Boolean,
type: Object as PropType<ITask>, default: false,
required: true,
},
isArchived: {
type: Boolean,
default: false,
},
showList: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
showListColor: {
type: Boolean,
default: true,
},
canMarkAsDone: {
type: Boolean,
default: true,
},
}, },
emits: ['task-updated'], disabled: {
watch: { type: Boolean,
theTask(newVal) { default: false,
this.task = newVal
},
}, },
mounted() { showListColor: {
this.task = this.theTask type: Boolean,
document.addEventListener('click', this.hideDeferDueDatePopup) default: true,
}, },
beforeUnmount() { canMarkAsDone: {
document.removeEventListener('click', this.hideDeferDueDatePopup) type: Boolean,
}, default: true,
computed: {
...mapState(useListStore, {
getListById: 'getListById',
}),
listColor() {
const list = this.getListById(this.task.listId)
return list !== null ? list.hexColor : ''
},
currentList() {
const baseStore = useBaseStore()
return typeof baseStore.currentList === 'undefined' ? {
id: 0,
title: '',
} : 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 updateFunc = async () => {
const task = await useTaskStore().update(this.task)
this.task = task
this.$emit('task-updated', task)
this.$message.success({
message: this.task.done ?
this.$t('task.doneSuccess') :
this.$t('task.undoneSuccess'),
}, [{
title: 'Undo',
callback: () => this.undoDone(checked),
}])
}
if (checked) {
setTimeout(updateFunc, 300) // Delay it to show the animation when marking a task as done
} else {
await updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
}
},
undoDone(checked: boolean) {
this.task.done = !this.task.done
this.markAsDone(!checked)
},
async toggleFavorite() {
this.task.isFavorite = !this.task.isFavorite
this.task = await this.taskService.update(this.task)
this.$emit('task-updated', this.task)
useNamespaceStore().loadNamespacesIfFavoritesDontExist()
},
hideDeferDueDatePopup(e) {
if (!this.showDefer) {
return
}
closeWhenClickedOutside(e, this.$refs.deferDueDate.$el, () => {
this.showDefer = false
})
},
}, },
}) })
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
},
)
onMounted(() => {
task.value = theTask.value
document.addEventListener('click', hideDeferDueDatePopup)
})
onBeforeUnmount(() => {
document.removeEventListener('click', hideDeferDueDatePopup)
})
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' ? {
id: 0,
title: '',
} : baseStore.currentList
})
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 newTask = await taskStore.update(task.value)
task.value = newTask
emit('task-updated', newTask)
success({
message: task.value.done ?
t('task.doneSuccess') :
t('task.undoneSuccess'),
}, [{
title: 'Undo',
callback: () => undoDone(checked),
}])
}
if (checked) {
setTimeout(updateFunc, 300) // Delay it to show the animation when marking a task as done
} else {
await updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
}
}
function undoDone(checked: boolean) {
task.value.done = !task.value.done
markAsDone(!checked)
}
async function toggleFavorite() {
task.value.isFavorite = !task.value.isFavorite
task.value = await taskService.update(task.value)
emit('task-updated', task.value)
namespaceStore.loadNamespacesIfFavoritesDontExist()
}
const deferDueDate = ref<typeof DeferTask | null>(null)
function hideDeferDueDatePopup(e) {
if (!showDefer.value) {
return
}
closeWhenClickedOutside(e, deferDueDate.value.$el, () => {
showDefer.value = false
})
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>