Files
ulitka-app/src/components/IssueDetails.vue
T
vadim 0d310c2732
Gitea Actions Demo / Deploy (push) Successful in 36s
add file upload for issue
2026-10-08 18:38:25 +03:00

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>