Files
speckle-connectors-dui/components/issues/SelectedItem.vue
T
2026-03-04 20:38:12 +02:00

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>
&nbsp;created this issue &middot;
{{ 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>
&nbsp;replied &middot;
{{ 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>