213 lines
5.7 KiB
Vue
213 lines
5.7 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, useTemplateRef, watchEffect } from 'vue'
|
|
import { useIssueStore } from '@/stores/issue.js'
|
|
import ExpansionPanel from '@/components/ExpansionPanel.vue'
|
|
import IssueContent from '@/components/IssueContent.vue'
|
|
import type { Issue } from '@/api/issue.api.ts'
|
|
import StatusMenu from '@/components/StatusMenu.vue'
|
|
import AssigneeMenu from '@/components/AssigneeMenu.vue'
|
|
import ReporterInfo from '@/components/ReporterInfo.vue'
|
|
import IssueTitle from '@/components/IssueTitle.vue'
|
|
import IssueDate from '@/components/IssueDate.vue'
|
|
import CommentsList from '@/components/CommentsList.vue'
|
|
import HistoryList from '@/components/HistoryList.vue'
|
|
import SplitHandle from '@/components/SplitHandle.vue'
|
|
import { useSplitPane } from '@/composables/useSplitPane.ts'
|
|
import FileUpload from '@/components/FileUpload.vue'
|
|
|
|
interface Props {
|
|
ckey: string
|
|
}
|
|
|
|
const emit = defineEmits<{
|
|
statusChanged: [issue: Issue]
|
|
assigneeChanged: [issue: Issue]
|
|
titleChanged: [issue: Issue]
|
|
dueDateChanged: [issue: Issue]
|
|
}>()
|
|
|
|
const issueStore = useIssueStore()
|
|
const issue = ref<Issue>()
|
|
const activity = ref()
|
|
|
|
const { ckey } = defineProps<Props>()
|
|
|
|
const splitContainer = useTemplateRef<HTMLElement>('splitContainer')
|
|
const { gridTemplate, handleProps } = useSplitPane(splitContainer, { storageKey: 'issue-details', min: 0.3, max: 0.7 })
|
|
|
|
const createdAtFormatted = computed(() => {
|
|
if (!issue.value?.createdAt) return ''
|
|
return new Date(issue.value.createdAt).toLocaleString('ru-RU')
|
|
})
|
|
|
|
async function fetch() {
|
|
const res = await issueStore.getByKey(ckey)
|
|
if (!res) {
|
|
return
|
|
}
|
|
|
|
issue.value = res
|
|
}
|
|
|
|
watchEffect(async () => fetch())
|
|
|
|
const onTitleChanged = async (title: string) => {
|
|
if (!issue.value) return
|
|
const updated = await issueStore.update(issue.value.id, { title })
|
|
if (updated) {
|
|
issue.value = updated
|
|
emit('titleChanged', updated)
|
|
}
|
|
}
|
|
|
|
const onDueDateChanged = async (date: string) => {
|
|
if (!issue.value) return
|
|
const updated = await issueStore.update(issue.value.id, { dueDate: date })
|
|
if (updated) {
|
|
issue.value = updated
|
|
emit('dueDateChanged', updated)
|
|
}
|
|
}
|
|
|
|
const onContentChanged = async (content: string) => {
|
|
if (!issue.value) return
|
|
const updated = await issueStore.update(issue.value.id, { content })
|
|
if (updated) issue.value = updated
|
|
}
|
|
|
|
const onStatusChanged = async (status: Issue['status']) => {
|
|
if (!issue.value) return
|
|
|
|
const updated = await issueStore.update(issue.value.id, { status })
|
|
if (updated) {
|
|
issue.value = updated
|
|
emit('statusChanged', updated)
|
|
}
|
|
}
|
|
|
|
const onAssigneeChanged = async (assignee: string) => {
|
|
if (!issue.value) return
|
|
|
|
const updated = await issueStore.update(issue.value.id, { assignee })
|
|
if (updated) {
|
|
issue.value = updated
|
|
emit('assigneeChanged', updated)
|
|
}
|
|
}
|
|
|
|
defineExpose({
|
|
applyUpdate: (updated: Issue) => {
|
|
if (issue.value && issue.value.id === updated.id) {
|
|
issue.value = updated
|
|
}
|
|
},
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="issue" ref="splitContainer" class="pa-4 details">
|
|
<div class="d-flex flex-column ga-4 details__main">
|
|
<div>{{ ckey }}</div>
|
|
|
|
<issue-title :text="issue.title" @changed="onTitleChanged" />
|
|
|
|
<v-card elevation="0" border>
|
|
<v-card-text class="u-grid">
|
|
<div class="text-medium-emphasis">Status:</div>
|
|
<status-menu :value="issue.status" @changed="onStatusChanged" />
|
|
|
|
<div class="text-medium-emphasis">Reporter:</div>
|
|
<reporter-info :user="issue.reporter" />
|
|
|
|
<div class="text-medium-emphasis">Assignee:</div>
|
|
<assignee-menu :value="issue.assignee" :compact="false" @changed="onAssigneeChanged" />
|
|
|
|
<div class="text-medium-emphasis">Created At:</div>
|
|
<div>{{ createdAtFormatted }}</div>
|
|
|
|
<div class="text-medium-emphasis">Due Date:</div>
|
|
<issue-date :date="issue.dueDate" @changed="onDueDateChanged" />
|
|
</v-card-text>
|
|
</v-card>
|
|
|
|
<expansion-panel title="Details" class="details__content">
|
|
<issue-content :text="issue.content" @changed="onContentChanged" />
|
|
</expansion-panel>
|
|
|
|
<expansion-panel title="Attachments">
|
|
<file-upload :issue-id="issue.id" />
|
|
</expansion-panel>
|
|
</div>
|
|
<split-handle v-bind="handleProps" />
|
|
<div class="d-flex flex-column">
|
|
<expansion-panel title="Activity" class="activity">
|
|
<v-sheet class="activity__sheet">
|
|
<v-btn-toggle class="align-self-start" border divided v-model="activity" mandatory="force" density="compact">
|
|
<v-btn rounded="0" prepend-icon="mdi-chat" value="comment">Comments</v-btn>
|
|
<v-btn rounded="0" prepend-icon="mdi-history" value="history">History</v-btn>
|
|
</v-btn-toggle>
|
|
|
|
<div class="activity__window">
|
|
<comments-list v-if="activity === 'comment'" :issue-id="issue.id" class="h-100" />
|
|
<history-list v-else-if="activity === 'history'" />
|
|
</div>
|
|
</v-sheet>
|
|
</expansion-panel>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.details {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: v-bind(gridTemplate);
|
|
column-gap: 0.5rem;
|
|
}
|
|
|
|
.details > * {
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
// Children keep their content height; only the Details panel shrinks to the space left and scrolls
|
|
.details__main > * {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.details__content {
|
|
flex-shrink: 1;
|
|
min-height: 0;
|
|
|
|
:deep(.panel__body) {
|
|
overflow-y: auto;
|
|
}
|
|
}
|
|
|
|
.activity {
|
|
flex: 1;
|
|
min-height: 0;
|
|
|
|
&__sheet {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
&__window {
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
}
|
|
|
|
.u-grid {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
align-items: center;
|
|
column-gap: 1rem;
|
|
row-gap: 1.5rem;
|
|
}
|
|
</style>
|