196 lines
6.1 KiB
Vue
196 lines
6.1 KiB
Vue
<template>
|
|
<div class="flex flex-col space-y-1.5">
|
|
<div class="flex flex-col items-start space-y-2 p-2">
|
|
<div class="line-clamp-2 font-medium text-body text-foreground">
|
|
{{ issue.title ? issue.title : 'No title' }}
|
|
</div>
|
|
<IssuesBasicTiptap
|
|
v-if="issue.description?.doc"
|
|
class="border rounded-xl border-outline-3 w-full"
|
|
:doc="issue.description?.doc"
|
|
></IssuesBasicTiptap>
|
|
|
|
<div v-if="app.$parametersBinding && hasObjectDeltas" class="w-full pt-1 pb-1">
|
|
<FormButton
|
|
class="w-full justify-center"
|
|
:disabled="isApplying || isResolved"
|
|
@click="applyChanges"
|
|
>
|
|
{{
|
|
isApplying ? 'Applying...' : isResolved ? 'Issue resolved' : 'Apply changes'
|
|
}}
|
|
</FormButton>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
|
<IssuesStatusIcon :status="issue.status" show-label />
|
|
<IssuesPriorityIcon :priority="issue.priority" show-label />
|
|
<div class="flex items-center justify-between space-x-1">
|
|
<UserAvatar :user="issue.assignee?.user" size="xs" />
|
|
<span class="text-body-3xs text-foreground-2 font-medium">
|
|
{{ issue.assignee ? issue.assignee?.user.name : 'No assignee' }}
|
|
</span>
|
|
</div>
|
|
<IssuesLabels :labels="issue.labels" />
|
|
<div v-if="formattedDate" class="flex items-center gap-1 h-6">
|
|
<Calendar class="text-foreground-2 shrink-0" :stroke-width="1.5" :size="12" />
|
|
<span class="text-body-3xs text-foreground-2 font-medium">
|
|
{{ formattedDate }}
|
|
</span>
|
|
</div>
|
|
<div v-else class="flex items-center gap-1 h-6">
|
|
<Calendar class="text-foreground-2 shrink-0" :stroke-width="1.5" :size="12" />
|
|
<span class="text-body-3xs text-foreground-2 font-medium">No due date</span>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="issue.activities && issue.activities.totalCount > 0"
|
|
class="flex items-center gap-2 p-1 min-w-0"
|
|
>
|
|
<UserAvatar
|
|
:user="issue.activities?.items?.[0]?.actor?.user"
|
|
size="xs"
|
|
class="shrink-0"
|
|
/>
|
|
|
|
<div class="text-body-2xs text-foreground-2 leading-tight min-w-0">
|
|
<span class="font-medium">
|
|
{{ issue.activities?.items?.[0]?.actor?.user.name }}
|
|
</span>
|
|
<span>
|
|
created this issue ·
|
|
{{ dayjs(issue.activities?.items?.[0].createdAt).from(dayjs()) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="issue.replies && issue.replies.totalCount > 0"
|
|
class="flex flex-col justify-between space-y-2 w-full"
|
|
>
|
|
<div
|
|
v-for="reply in issue.replies.items"
|
|
:key="reply.id"
|
|
class="flex flex-col items-start border rounded-xl border-outline-3 p-1 w-full"
|
|
>
|
|
<div class="flex items-center gap-2 w-full">
|
|
<UserAvatar :user="reply.author?.user" size="xs" class="shrink-0" />
|
|
<div class="text-body-2xs text-foreground-2 leading-tight min-w-0">
|
|
<span class="font-medium">
|
|
{{ reply.author?.user.name }}
|
|
</span>
|
|
<span>
|
|
replied ·
|
|
{{ dayjs(reply.createdAt).from(dayjs()) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<IssuesBasicTiptap
|
|
v-if="reply.description?.doc"
|
|
class="ml-4"
|
|
:doc="reply.description?.doc"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
import { useQuery } from '@vue/apollo-composable'
|
|
import { ResourceMetaType, IssueStatus } from '~/lib/common/generated/gql/graphql'
|
|
import { issueResourceMetaSearchQuery } from '~/lib/issues/graphql/queries'
|
|
import type { IssuesItemFragment } from '~/lib/common/generated/gql/graphql'
|
|
import type { IModelCard } from '~/lib/models/card'
|
|
import dayjs from 'dayjs'
|
|
import { Calendar } from 'lucide-vue-next'
|
|
|
|
const props = defineProps<{
|
|
issue: IssuesItemFragment
|
|
modelCard: IModelCard
|
|
}>()
|
|
|
|
const app = useNuxtApp()
|
|
const isApplying = ref(false)
|
|
|
|
const isResolved = computed(() => {
|
|
return props.issue.status === IssueStatus.Resolved
|
|
})
|
|
|
|
const queryVariables = computed(() => ({
|
|
workspaceId: props.modelCard.workspaceId!,
|
|
projectId: props.modelCard.projectId,
|
|
resourceType: ResourceMetaType.Issue,
|
|
resourceId: props.issue.id,
|
|
metaType: 'objectDeltas'
|
|
}))
|
|
|
|
const queryOptions = computed(() => ({
|
|
fetchPolicy: 'cache-and-network' as const,
|
|
enabled: !!props.modelCard.workspaceId,
|
|
clientId: props.modelCard.accountId
|
|
}))
|
|
|
|
const { result: resourceMetaResult } = useQuery(
|
|
issueResourceMetaSearchQuery,
|
|
queryVariables,
|
|
queryOptions
|
|
)
|
|
|
|
const hasObjectDeltas = computed<boolean>(() => {
|
|
const metadata = resourceMetaResult.value?.resourceMetaSearch
|
|
return Array.isArray(metadata) && metadata.length > 0
|
|
})
|
|
|
|
const objectDeltasPayload = computed<unknown>(() => {
|
|
if (!hasObjectDeltas.value) return null
|
|
const metadata = resourceMetaResult.value?.resourceMetaSearch
|
|
|
|
if (Array.isArray(metadata) && metadata.length > 0) {
|
|
return metadata[0]?.data as unknown
|
|
}
|
|
|
|
return null
|
|
})
|
|
|
|
const applyChanges = async () => {
|
|
if (!objectDeltasPayload.value) return
|
|
|
|
isApplying.value = true
|
|
try {
|
|
const payload =
|
|
typeof objectDeltasPayload.value === 'string'
|
|
? objectDeltasPayload.value
|
|
: JSON.stringify(objectDeltasPayload.value)
|
|
|
|
if (app.$parametersBinding) {
|
|
await app.$parametersBinding.update(payload)
|
|
} else {
|
|
console.warn('IParametersBinding not available in this host app')
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to apply changes:', error)
|
|
} finally {
|
|
isApplying.value = false
|
|
}
|
|
}
|
|
|
|
const formattedDate = computed((): string | null => {
|
|
try {
|
|
const date = props.issue.dueDate ? dayjs(props.issue.dueDate).toDate() : null
|
|
|
|
if (!(date instanceof Date)) return null
|
|
|
|
const time = date.getTime()
|
|
if (isNaN(time)) return null
|
|
|
|
return new Intl.DateTimeFormat('en-GB', {
|
|
month: 'short',
|
|
day: 'numeric'
|
|
}).format(date)
|
|
} catch {
|
|
return null
|
|
}
|
|
})
|
|
</script>
|