This repository has been archived on 2024-02-08. You can view files and clone it, but cannot push or open issues or pull requests.
frontend/src/views/tasks/TaskDetailView.vue

1146 lines
28 KiB
Vue
Raw Permalink Normal View History

2019-11-24 13:16:24 +00:00
<template>
<div
class="loader-container task-view-container"
:class="{
'is-loading': taskService.loading || !visible,
'is-modal': isModal,
}"
>
<!-- Removing everything until the task is loaded to prevent empty initialization of other components -->
2024-02-07 11:18:19 +00:00
<div
v-if="visible"
class="task-view"
>
<Heading
2024-02-07 11:18:19 +00:00
ref="heading"
:task="task"
:can-write="canWrite"
2024-02-07 11:18:19 +00:00
@update:task="Object.assign(task, $event)"
/>
2024-02-07 11:18:19 +00:00
<h6
v-if="project?.id"
class="subtitle"
>
<template
v-for="p in projectStore.getAncestors(project)"
:key="p.id"
>
<router-link :to="{ name: 'project.index', params: { projectId: p.id } }">
{{ getProjectTitle(p) }}
</router-link>
2024-02-07 11:18:19 +00:00
<span
v-if="p.id !== project?.id"
class="has-text-grey-light"
> &gt; </span>
</template>
</h6>
2024-02-07 11:18:19 +00:00
<ChecklistSummary :task="task" />
2019-11-24 13:16:24 +00:00
<!-- Content and buttons -->
<div class="columns mt-2">
2019-11-24 13:16:24 +00:00
<!-- Content -->
2024-02-07 11:18:19 +00:00
<div
:class="{'is-two-thirds': canWrite}"
class="column detail-content"
>
2019-11-24 13:16:24 +00:00
<div class="columns details">
2024-02-07 11:18:19 +00:00
<div
v-if="activeFields.assignees"
class="column assignees"
>
2019-11-24 13:16:24 +00:00
<!-- Assignees -->
<div class="detail-title">
2024-02-07 11:18:19 +00:00
<icon icon="users" />
{{ $t('task.attributes.assignees') }}
2019-11-24 13:16:24 +00:00
</div>
2024-02-07 11:18:19 +00:00
<EditAssignees
v-if="canWrite"
:ref="e => setFieldRef('assignees', e)"
v-model="task.assignees"
2024-02-07 11:18:19 +00:00
:project-id="task.projectId"
:task-id="task.id"
2019-11-24 13:16:24 +00:00
/>
2024-02-07 11:18:19 +00:00
<AssigneeList
v-else
:assignees="task.assignees"
class="mt-2"
/>
2019-11-24 13:16:24 +00:00
</div>
2024-02-07 11:18:19 +00:00
<CustomTransition
name="flash-background"
appear
>
<div
v-if="activeFields.priority"
class="column"
>
<!-- Priority -->
<div class="detail-title">
2024-02-07 11:18:19 +00:00
<icon icon="exclamation" />
{{ $t('task.attributes.priority') }}
</div>
2024-02-07 11:18:19 +00:00
<PrioritySelect
:ref="e => setFieldRef('priority', e)"
2024-02-07 11:18:19 +00:00
v-model="task.priority"
:disabled="!canWrite"
@update:modelValue="setPriority"
/>
2019-11-24 13:16:24 +00:00
</div>
2022-11-12 16:06:53 +00:00
</CustomTransition>
2024-02-07 11:18:19 +00:00
<CustomTransition
name="flash-background"
appear
>
<div
v-if="activeFields.dueDate"
class="column"
>
<!-- Due Date -->
<div class="detail-title">
2024-02-07 11:18:19 +00:00
<icon icon="calendar" />
{{ $t('task.attributes.dueDate') }}
</div>
<div class="date-input">
2024-02-07 11:18:19 +00:00
<Datepicker
:ref="e => setFieldRef('dueDate', e)"
v-model="task.dueDate"
:choose-date-label="$t('task.detail.chooseDueDate')"
:disabled="taskService.loading || !canWrite"
2024-02-07 11:18:19 +00:00
@closeOnChange="saveTask()"
/>
<BaseButton
v-if="task.dueDate && canWrite"
2024-02-07 11:18:19 +00:00
class="remove"
@click="() => {task.dueDate = null;saveTask()}"
2024-02-07 11:18:19 +00:00
>
<span class="icon is-small">
2024-02-07 11:18:19 +00:00
<icon icon="times" />
</span>
</BaseButton>
</div>
2019-11-24 13:16:24 +00:00
</div>
2022-11-12 16:06:53 +00:00
</CustomTransition>
2024-02-07 11:18:19 +00:00
<CustomTransition
name="flash-background"
appear
>
<div
v-if="activeFields.percentDone"
class="column"
>
<!-- Progress -->
<div class="detail-title">
2024-02-07 11:18:19 +00:00
<icon icon="percent" />
{{ $t('task.attributes.percentDone') }}
</div>
2024-02-07 11:18:19 +00:00
<PercentDoneSelect
:ref="e => setFieldRef('percentDone', e)"
2024-02-07 11:18:19 +00:00
v-model="task.percentDone"
:disabled="!canWrite"
@update:modelValue="setPercentDone"
/>
2019-11-24 13:16:24 +00:00
</div>
2022-11-12 16:06:53 +00:00
</CustomTransition>
2024-02-07 11:18:19 +00:00
<CustomTransition
name="flash-background"
appear
>
<div
v-if="activeFields.startDate"
class="column"
>
<!-- Start Date -->
<div class="detail-title">
2024-02-07 11:18:19 +00:00
<icon icon="play" />
{{ $t('task.attributes.startDate') }}
</div>
<div class="date-input">
2024-02-07 11:18:19 +00:00
<Datepicker
:ref="e => setFieldRef('startDate', e)"
v-model="task.startDate"
:choose-date-label="$t('task.detail.chooseStartDate')"
:disabled="taskService.loading || !canWrite"
2024-02-07 11:18:19 +00:00
@closeOnChange="saveTask()"
/>
<BaseButton
v-if="task.startDate && canWrite"
class="remove"
2024-02-07 11:18:19 +00:00
@click="() => {task.startDate = null;saveTask()}"
>
<span class="icon is-small">
2024-02-07 11:18:19 +00:00
<icon icon="times" />
</span>
</BaseButton>
</div>
2019-11-24 13:16:24 +00:00
</div>
2022-11-12 16:06:53 +00:00
</CustomTransition>
2024-02-07 11:18:19 +00:00
<CustomTransition
name="flash-background"
appear
>
<div
v-if="activeFields.endDate"
class="column"
>
<!-- End Date -->
<div class="detail-title">
2024-02-07 11:18:19 +00:00
<icon icon="stop" />
{{ $t('task.attributes.endDate') }}
</div>
<div class="date-input">
2024-02-07 11:18:19 +00:00
<Datepicker
:ref="e => setFieldRef('endDate', e)"
v-model="task.endDate"
:choose-date-label="$t('task.detail.chooseEndDate')"
:disabled="taskService.loading || !canWrite"
2024-02-07 11:18:19 +00:00
@closeOnChange="saveTask()"
/>
<BaseButton
v-if="task.endDate && canWrite"
2024-02-07 11:18:19 +00:00
class="remove"
@click="() => {task.endDate = null;saveTask()}"
>
<span class="icon is-small">
2024-02-07 11:18:19 +00:00
<icon icon="times" />
</span>
</BaseButton>
</div>
2019-11-24 13:16:24 +00:00
</div>
2022-11-12 16:06:53 +00:00
</CustomTransition>
2024-02-07 11:18:19 +00:00
<CustomTransition
name="flash-background"
appear
>
<div
v-if="activeFields.reminders"
class="column"
>
<!-- Reminders -->
<div class="detail-title">
2024-02-07 11:18:19 +00:00
<icon :icon="['far', 'clock']" />
{{ $t('task.attributes.reminders') }}
</div>
2024-02-07 11:18:19 +00:00
<Reminders
:ref="e => setFieldRef('reminders', e)"
v-model="task"
2024-02-07 11:18:19 +00:00
:disabled="!canWrite"
@update:modelValue="saveTask()"
/>
2019-11-24 13:16:24 +00:00
</div>
2022-11-12 16:06:53 +00:00
</CustomTransition>
2024-02-07 11:18:19 +00:00
<CustomTransition
name="flash-background"
appear
>
<div
v-if="activeFields.repeatAfter"
class="column"
>
<!-- Repeat after -->
<div class="is-flex is-justify-content-space-between">
<div class="detail-title">
2024-02-07 11:18:19 +00:00
<icon icon="history" />
{{ $t('task.attributes.repeat') }}
</div>
<BaseButton
v-if="canWrite"
2024-02-07 11:18:19 +00:00
class="remove"
@click="removeRepeatAfter"
>
<span class="icon is-small">
2024-02-07 11:18:19 +00:00
<icon icon="times" />
</span>
</BaseButton>
</div>
2024-02-07 11:18:19 +00:00
<RepeatAfter
:ref="e => setFieldRef('repeatAfter', e)"
v-model="task"
2024-02-07 11:18:19 +00:00
:disabled="!canWrite"
@update:modelValue="saveTask()"
/>
2019-11-24 13:16:24 +00:00
</div>
2022-11-12 16:06:53 +00:00
</CustomTransition>
2024-02-07 11:18:19 +00:00
<CustomTransition
name="flash-background"
appear
>
<div
v-if="activeFields.color"
class="column"
>
<!-- Color -->
<div class="detail-title">
2024-02-07 11:18:19 +00:00
<icon icon="fill-drip" />
{{ $t('task.attributes.color') }}
</div>
2024-02-07 11:18:19 +00:00
<ColorPicker
:ref="e => setFieldRef('color', e)"
v-model="taskColor"
2024-02-07 11:18:19 +00:00
menu-position="bottom"
@update:modelValue="saveTask()"
/>
</div>
2022-11-12 16:06:53 +00:00
</CustomTransition>
2019-11-24 13:16:24 +00:00
</div>
<!-- Labels -->
2024-02-07 11:18:19 +00:00
<div
v-if="activeFields.labels"
class="labels-list details"
>
2019-11-24 13:16:24 +00:00
<div class="detail-title">
<span class="icon is-grey">
2024-02-07 11:18:19 +00:00
<icon icon="tags" />
2019-11-24 13:16:24 +00:00
</span>
{{ $t('task.attributes.labels') }}
2019-11-24 13:16:24 +00:00
</div>
2024-02-07 11:18:19 +00:00
<EditLabels
:ref="e => setFieldRef('labels', e)"
v-model="task.labels"
:disabled="!canWrite"
:task-id="taskId"
2024-02-07 11:18:19 +00:00
/>
2019-11-24 13:16:24 +00:00
</div>
<!-- Description -->
2021-01-16 20:55:43 +00:00
<div class="details content description">
2024-02-07 11:18:19 +00:00
<Description
:model-value="task"
:can-write="canWrite"
:attachment-upload="attachmentUpload"
2024-02-07 11:18:19 +00:00
@update:modelValue="Object.assign(task, $event)"
/>
2019-11-24 13:16:24 +00:00
</div>
<!-- Attachments -->
2024-02-07 11:18:19 +00:00
<div
v-if="activeFields.attachments || hasAttachments"
class="content attachments"
>
<Attachments
:ref="e => setFieldRef('attachments', e)"
:edit-enabled="canWrite"
:task="task"
2024-02-07 11:18:19 +00:00
@taskChanged="({coverImageAttachmentId}) => task.coverImageAttachmentId = coverImageAttachmentId"
2019-11-24 13:16:24 +00:00
/>
</div>
<!-- Related Tasks -->
2024-02-07 11:18:19 +00:00
<div
v-if="activeFields.relatedTasks"
class="content details mb-0"
>
2019-11-24 13:16:24 +00:00
<h3>
<span class="icon is-grey">
2024-02-07 11:18:19 +00:00
<icon icon="sitemap" />
2019-11-24 13:16:24 +00:00
</span>
{{ $t('task.attributes.relatedTasks') }}
2019-11-24 13:16:24 +00:00
</h3>
2024-02-07 11:18:19 +00:00
<RelatedTasks
:ref="e => setFieldRef('relatedTasks', e)"
:edit-enabled="canWrite"
:initial-related-tasks="task.relatedTasks"
:project-id="task.projectId"
:show-no-relations-notice="true"
:task-id="taskId"
2019-11-24 13:16:24 +00:00
/>
</div>
2020-04-18 12:39:56 +00:00
<!-- Move Task -->
2024-02-07 11:18:19 +00:00
<div
v-if="activeFields.moveProject"
class="content details"
>
2020-04-18 12:39:56 +00:00
<h3>
<span class="icon is-grey">
2024-02-07 11:18:19 +00:00
<icon icon="list" />
2020-04-18 12:39:56 +00:00
</span>
{{ $t('task.detail.move') }}
2020-04-18 12:39:56 +00:00
</h3>
<div class="field has-addons">
<div class="control is-expanded">
2024-02-07 11:18:19 +00:00
<ProjectSearch
:ref="e => setFieldRef('moveProject', e)"
2024-02-07 11:18:19 +00:00
@update:modelValue="changeProject"
/>
2020-04-18 12:39:56 +00:00
</div>
</div>
</div>
<!-- Comments -->
2024-02-07 11:18:19 +00:00
<Comments
:can-write="canWrite"
:task-id="taskId"
/>
2019-11-24 13:16:24 +00:00
</div>
<!-- Task Actions -->
2024-02-07 11:18:19 +00:00
<div
v-if="canWrite || isModal"
class="column is-one-third action-buttons d-print-none"
>
<template v-if="canWrite">
<x-button
2024-02-07 11:18:19 +00:00
v-shortcut="'t'"
:class="{'is-success': !task.done}"
:shadow="task.done"
class="is-outlined has-no-border"
icon="check-double"
variant="secondary"
2024-02-07 11:18:19 +00:00
@click="toggleTaskDone()"
>
{{ task.done ? $t('task.detail.undone') : $t('task.detail.done') }}
</x-button>
2024-02-07 11:18:19 +00:00
<TaskSubscription
entity="task"
:entity-id="task.id"
:model-value="task.subscription"
2024-02-07 11:18:19 +00:00
@update:modelValue="sub => task.subscription = sub"
/>
<x-button
2024-02-07 11:18:19 +00:00
v-shortcut="'s'"
variant="secondary"
:icon="task.isFavorite ? 'star' : ['far', 'star']"
2024-02-07 11:18:19 +00:00
@click="toggleFavorite"
>
{{
task.isFavorite ? $t('task.detail.actions.unfavorite') : $t('task.detail.actions.favorite')
}}
</x-button>
<span class="action-heading">{{ $t('task.detail.organization') }}</span>
<x-button
2024-02-07 11:18:19 +00:00
v-shortcut="'l'"
variant="secondary"
icon="tags"
2024-02-07 11:18:19 +00:00
@click="setFieldActive('labels')"
>
{{ $t('task.detail.actions.label') }}
</x-button>
<x-button
2024-02-07 11:18:19 +00:00
v-shortcut="'p'"
variant="secondary"
icon="exclamation"
2024-02-07 11:18:19 +00:00
@click="setFieldActive('priority')"
>
{{ $t('task.detail.actions.priority') }}
</x-button>
<x-button
variant="secondary"
icon="percent"
2024-02-07 11:18:19 +00:00
@click="setFieldActive('percentDone')"
>
{{ $t('task.detail.actions.percentDone') }}
</x-button>
<x-button
2024-02-07 11:18:19 +00:00
v-shortcut="'c'"
variant="secondary"
icon="fill-drip"
:icon-color="color"
2024-02-07 11:18:19 +00:00
@click="setFieldActive('color')"
>
{{ $t('task.detail.actions.color') }}
</x-button>
<span class="action-heading">{{ $t('task.detail.management') }}</span>
<x-button
v-shortcut="'a'"
v-cy="'taskDetail.assign'"
2024-02-07 11:18:19 +00:00
variant="secondary"
@click="setFieldActive('assignees')"
>
2024-02-07 11:18:19 +00:00
<span class="icon is-small"><icon icon="users" /></span>
{{ $t('task.detail.actions.assign') }}
</x-button>
<x-button
2024-02-07 11:18:19 +00:00
v-shortcut="'f'"
variant="secondary"
icon="paperclip"
2024-02-07 11:18:19 +00:00
@click="setFieldActive('attachments')"
>
{{ $t('task.detail.actions.attachments') }}
</x-button>
<x-button
2024-02-07 11:18:19 +00:00
v-shortcut="'r'"
variant="secondary"
icon="sitemap"
2024-02-07 11:18:19 +00:00
@click="setRelatedTasksActive()"
>
{{ $t('task.detail.actions.relatedTasks') }}
</x-button>
<x-button
2024-02-07 11:18:19 +00:00
v-shortcut="'m'"
variant="secondary"
icon="list"
2024-02-07 11:18:19 +00:00
@click="setFieldActive('moveProject')"
>
{{ $t('task.detail.actions.moveProject') }}
</x-button>
<span class="action-heading">{{ $t('task.detail.dateAndTime') }}</span>
<x-button
2024-02-07 11:18:19 +00:00
v-shortcut="'d'"
variant="secondary"
icon="calendar"
2024-02-07 11:18:19 +00:00
@click="setFieldActive('dueDate')"
>
{{ $t('task.detail.actions.dueDate') }}
</x-button>
<x-button
variant="secondary"
icon="play"
2024-02-07 11:18:19 +00:00
@click="setFieldActive('startDate')"
>
{{ $t('task.detail.actions.startDate') }}
</x-button>
<x-button
variant="secondary"
icon="stop"
2024-02-07 11:18:19 +00:00
@click="setFieldActive('endDate')"
>
{{ $t('task.detail.actions.endDate') }}
</x-button>
<x-button
2024-02-07 11:18:19 +00:00
v-shortcut="'Alt+r'"
variant="secondary"
:icon="['far', 'clock']"
2024-02-07 11:18:19 +00:00
@click="setFieldActive('reminders')"
>
{{ $t('task.detail.actions.reminders') }}
</x-button>
<x-button
variant="secondary"
icon="history"
2024-02-07 11:18:19 +00:00
@click="setFieldActive('repeatAfter')"
>
{{ $t('task.detail.actions.repeatAfter') }}
</x-button>
<x-button
2024-02-07 11:18:19 +00:00
v-shortcut="'Shift+Delete'"
icon="trash-alt"
:shadow="false"
class="is-danger is-outlined has-no-border"
2024-02-07 11:18:19 +00:00
@click="showDeleteModal = true"
>
{{ $t('task.detail.actions.delete') }}
</x-button>
</template>
2020-10-25 11:25:08 +00:00
<!-- Created / Updated [by] -->
2024-02-07 11:18:19 +00:00
<CreatedUpdated :task="task" />
2019-11-24 13:16:24 +00:00
</div>
</div>
<!-- Created / Updated [by] -->
2024-02-07 11:18:19 +00:00
<CreatedUpdated
v-if="!canWrite && !isModal"
:task="task"
/>
2019-11-24 13:16:24 +00:00
</div>
<modal
:enabled="showDeleteModal"
@close="showDeleteModal = false"
@submit="deleteTask()"
>
2024-02-07 11:18:19 +00:00
<template #header>
<span>{{ $t('task.detail.delete.header') }}</span>
</template>
<template #text>
2024-02-07 11:18:19 +00:00
<p>
{{ $t('task.detail.delete.text1') }}<br>
{{ $t('task.detail.delete.text2') }}
</p>
</template>
</modal>
2019-11-24 13:16:24 +00:00
</div>
</template>
<script lang="ts" setup>
2024-02-07 11:18:19 +00:00
import {ref, reactive, shallowReactive, computed, watch, nextTick, onMounted, onBeforeUnmount} from 'vue'
import {useRouter, type RouteLocation} from 'vue-router'
import {useI18n} from 'vue-i18n'
import {unrefElement} from '@vueuse/core'
import {klona} from 'klona/lite'
import {eventToHotkeyString} from '@github/hotkey'
2022-02-15 12:07:59 +00:00
import TaskService from '@/services/task'
import TaskModel from '@/models/task'
2022-09-06 09:36:01 +00:00
import type {ITask} from '@/modelTypes/ITask'
import type {IProject} from '@/modelTypes/IProject'
import {PRIORITIES, type Priority} from '@/constants/priorities'
import {RIGHTS} from '@/constants/rights'
2019-11-24 13:16:24 +00:00
2022-06-23 01:08:35 +00:00
import BaseButton from '@/components/base/BaseButton.vue'
// partials
import Attachments from '@/components/tasks/partials/attachments.vue'
import ChecklistSummary from '@/components/tasks/partials/checklist-summary.vue'
2022-11-17 16:05:10 +00:00
import ColorPicker from '@/components/input/ColorPicker.vue'
import Comments from '@/components/tasks/partials/comments.vue'
import CreatedUpdated from '@/components/tasks/partials/createdUpdated.vue'
import Datepicker from '@/components/input/datepicker.vue'
import Description from '@/components/tasks/partials/description.vue'
import EditAssignees from '@/components/tasks/partials/editAssignees.vue'
import EditLabels from '@/components/tasks/partials/editLabels.vue'
import Heading from '@/components/tasks/partials/heading.vue'
import ProjectSearch from '@/components/tasks/partials/projectSearch.vue'
import PercentDoneSelect from '@/components/tasks/partials/percentDoneSelect.vue'
import PrioritySelect from '@/components/tasks/partials/prioritySelect.vue'
import RelatedTasks from '@/components/tasks/partials/relatedTasks.vue'
import Reminders from '@/components/tasks/partials/reminders.vue'
import RepeatAfter from '@/components/tasks/partials/repeatAfter.vue'
import TaskSubscription from '@/components/misc/subscription.vue'
2022-11-12 16:06:53 +00:00
import CustomTransition from '@/components/misc/CustomTransition.vue'
2019-11-24 13:16:24 +00:00
import {uploadFile} from '@/helpers/attachments'
import {getProjectTitle} from '@/helpers/getProjectTitle'
import {scrollIntoView} from '@/helpers/scrollIntoView'
2022-09-02 09:23:47 +00:00
import {useAttachmentStore} from '@/stores/attachments'
2022-09-23 10:55:53 +00:00
import {useTaskStore} from '@/stores/tasks'
2022-09-24 10:35:41 +00:00
import {useKanbanStore} from '@/stores/kanban'
2021-11-30 19:48:48 +00:00
import {useTitle} from '@/composables/useTitle'
import {success} from '@/message'
import type {Action as MessageAction} from '@/message'
2023-03-25 13:54:20 +00:00
import {useProjectStore} from '@/stores/projects'
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
import {useAuthStore} from '@/stores/auth'
import {playPopSound} from '@/helpers/playPop'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
const {
taskId,
backdropView,
} = defineProps<{
taskId: ITask['id'],
backdropView?: RouteLocation['fullPath'],
}>()
2021-12-10 14:29:28 +00:00
defineEmits(['close'])
const router = useRouter()
const {t} = useI18n({useScope: 'global'})
2021-12-10 14:29:28 +00:00
2023-03-25 13:54:20 +00:00
const projectStore = useProjectStore()
const attachmentStore = useAttachmentStore()
const taskStore = useTaskStore()
const kanbanStore = useKanbanStore()
const task = ref<ITask>(new TaskModel())
2024-02-07 11:18:19 +00:00
const taskTitle = computed(() => task.value.title)
useTitle(taskTitle)
// See https://github.com/github/hotkey/discussions/85#discussioncomment-5214660
function saveTaskViaHotkey(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 visible = 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()
2022-10-17 11:14:07 +00:00
: undefined
return color
})
const hasAttachments = computed(() => attachmentStore.attachments.length > 0)
const isModal = computed(() => Boolean(backdropView))
function attachmentUpload(file: File, onSuccess?: (url: string) => void) {
return uploadFile(taskId, file, onSuccess)
}
const heading = ref<HTMLElement | null>(null)
async function scrollToHeading() {
scrollIntoView(unrefElement(heading))
}
const taskService = shallowReactive(new TaskService())
// load task
watch(
() => 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()
visible.value = true
}
}, {immediate: true})
type FieldType =
| 'assignees'
| 'attachments'
| 'color'
| 'dueDate'
| 'endDate'
| 'labels'
| 'moveProject'
| 'percentDone'
| 'priority'
| 'relatedTasks'
| 'reminders'
| 'repeatAfter'
| 'startDate'
const activeFields: { [type in FieldType]: boolean } = reactive({
assignees: false,
attachments: false,
color: false,
dueDate: false,
endDate: false,
labels: false,
moveProject: false,
percentDone: false,
priority: false,
relatedTasks: false,
reminders: false,
repeatAfter: false,
startDate: false,
})
function setActiveFields() {
// FIXME: are these lines necessary?
// task.startDate = task.startDate || null
// 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
}
const activeFieldElements: { [id in FieldType]: HTMLElement | null } = reactive({
assignees: null,
attachments: null,
color: null,
dueDate: null,
endDate: null,
labels: null,
moveProject: null,
percentDone: null,
priority: null,
relatedTasks: null,
reminders: null,
repeatAfter: null,
startDate: null,
})
function setFieldRef(name, e) {
activeFieldElements[name] = unrefElement(e)
}
function setFieldActive(fieldName: keyof typeof activeFields) {
activeFields[fieldName] = true
nextTick(() => {
const el = activeFieldElements[fieldName]
if (!el) {
return
}
el.focus()
// scroll the field to the center of the screen if not in viewport already
scrollIntoView(el)
})
}
async function saveTask(
currentTask: ITask | null = null,
undoCallback?: () => void,
) {
if (currentTask === null) {
currentTask = klona(task.value)
}
if (!canWrite.value) {
return
}
2022-09-05 16:06:55 +00:00
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}})
}
function toggleTaskDone() {
const newTask = {
...task.value,
done: !task.value.done,
}
if (newTask.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
playPopSound()
}
saveTask(
newTask,
toggleTaskDone,
)
}
async function changeProject(project: IProject) {
kanbanStore.removeTaskInBucket(task.value)
await saveTask({
...task.value,
projectId: project.id,
})
}
async function toggleFavorite() {
2023-03-28 15:25:34 +00:00
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')
// If the related tasks are already available, show the form again
const el = activeFieldElements['relatedTasks']
for (const child in el?.children) {
if (el?.children[child]?.id === 'showRelatedTasksFormButton') {
el?.children[child]?.click()
break
}
}
}
2019-11-24 13:16:24 +00:00
</script>
<style lang="scss" scoped>
.task-view-container {
// simulate sass lighten($primary, 30) by increasing lightness 30% to 73%
--primary-light: hsla(var(--primary-h), var(--primary-s), 73%, var(--primary-a));
padding-bottom: 0;
@media screen and (min-width: $desktop) {
padding-bottom: 1rem;
}
}
.task-view {
padding-top: 1rem;
padding-inline: .5rem;
background-color: var(--site-background);
@media screen and (min-width: $desktop) {
padding: 1rem;
}
}
2022-11-12 16:06:53 +00:00
.is-modal .task-view {
border-radius: $radius;
padding: 1rem;
color: var(--text);
background-color: var(--site-background) !important;
@media screen and (max-width: 800px) {
border-radius: 0;
padding-top: 2rem;
}
2022-11-12 16:06:53 +00:00
}
.task-view * {
transition: opacity 50ms ease;
}
.is-loading .task-view * {
opacity: 0;
}
.subtitle {
color: var(--grey-500);
margin-bottom: 1rem;
a {
color: var(--grey-800);
}
}
h3 .button {
vertical-align: middle;
}
.icon.is-grey {
color: var(--grey-400);
}
.date-input {
display: flex;
align-items: center;
}
.remove {
color: var(--danger);
vertical-align: middle;
padding-left: .5rem;
line-height: 1;
}
:deep(.datepicker) {
width: 100%;
.show {
color: var(--text);
padding: .25rem .5rem;
transition: background-color $transition;
border-radius: $radius;
display: block;
margin: .1rem 0;
width: 100%;
text-align: left;
&:hover {
background: var(--white);
}
}
&.disabled .show:hover {
background: transparent;
}
}
.details {
padding-bottom: 0.75rem;
flex-flow: row wrap;
margin-bottom: 0;
.detail-title {
display: block;
color: var(--grey-400);
}
.none {
font-style: italic;
}
// Break after the 2nd element
.column:nth-child(2n) {
page-break-after: always; // CSS 2.1 syntax
break-after: always; // New syntax
}
}
.details.labels-list,
.assignees {
:deep(.multiselect) {
.input-wrapper {
&:not(:focus-within):not(:hover) {
background: transparent;
border-color: transparent;
}
}
}
}
:deep(.details),
:deep(.heading) {
.input:not(.has-defaults),
.textarea,
.select:not(.has-defaults) select {
cursor: pointer;
transition: all $transition-duration;
&::placeholder {
color: var(--text-light);
opacity: 1;
font-style: italic;
}
&:not(:disabled) {
&:hover,
&:active,
&:focus {
background: var(--scheme-main);
border-color: var(--border);
cursor: text;
}
&:hover,
&:active {
cursor: text;
border-color: var(--link)
}
}
}
.select:not(.has-defaults):after {
opacity: 0;
}
.select:not(.has-defaults):hover:after {
opacity: 1;
}
}
.attachments {
margin-bottom: 0;
table tr:last-child td {
border-bottom: none;
}
}
.action-buttons {
@media screen and (min-width: $tablet) {
position: sticky;
top: $navbar-height + 1.5rem;
align-self: flex-start;
}
.button {
width: 100%;
margin-bottom: .5rem;
justify-content: left;
&.has-light-text {
color: var(--white);
}
}
}
.is-modal .action-buttons {
// we need same top margin for the modal close button
@media screen and (min-width: $tablet) {
top: 6.5rem;
}
// this is the moment when the fixed close button is outside the modal
// => we can fill up the space again
@media screen and (min-width: calc(#{$desktop} + 84px)) {
top: 0;
}
}
.checklist-summary {
padding-left: .25rem;
}
2022-06-02 21:00:21 +00:00
.detail-content {
@media print {
2022-11-12 16:06:53 +00:00
width: 100% !important;
}
2022-06-02 21:00:21 +00:00
}
.action-heading {
text-transform: uppercase;
color: var(--grey-700);
font-size: .75rem;
font-weight: 700;
margin: .5rem 0;
display: inline-block;
}
</style>