feat(#21): Replaced chat comment component with proper cursor-based commenting
This commit is contained in:
@@ -32,35 +32,58 @@
|
||||
:form-element-id="formElementItem.formElement.id"
|
||||
:application-form-id="applicationFormId"
|
||||
:comments="comments?.[formElementItem.formElement.id]"
|
||||
:total-count="
|
||||
commentCounts?.[formElementItem.formElement.id] ?? comments?.[formElementItem.formElement.id]?.length ?? 0
|
||||
"
|
||||
@close="activeFormElement = ''"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'transition-opacity duration-200',
|
||||
openDropdownId === getElementKey(formElementItem.formElement, formElementItem.indexInSubsection)
|
||||
? 'opacity-100'
|
||||
: 'opacity-0 group-hover:opacity-100'
|
||||
]"
|
||||
>
|
||||
<UDropdownMenu
|
||||
:items="
|
||||
getDropdownItems(
|
||||
formElementItem.formElement,
|
||||
getElementKey(formElementItem.formElement, formElementItem.indexInSubsection),
|
||||
formElementItem.indexInSubsection
|
||||
)
|
||||
"
|
||||
:content="{ align: 'end' }"
|
||||
@update:open="
|
||||
(isOpen: boolean) =>
|
||||
handleDropdownToggle(
|
||||
getElementKey(formElementItem.formElement, formElementItem.indexInSubsection),
|
||||
isOpen
|
||||
)
|
||||
"
|
||||
<div class="flex items-start gap-1">
|
||||
<div class="min-w-9">
|
||||
<UButton
|
||||
v-if="
|
||||
applicationFormId &&
|
||||
formElementItem.formElement.id &&
|
||||
(commentCounts?.[formElementItem.formElement.id] ?? 0) > 0
|
||||
"
|
||||
color="neutral"
|
||||
variant="soft"
|
||||
size="xs"
|
||||
icon="i-lucide-message-square"
|
||||
class="w-full justify-center"
|
||||
@click="toggleComments(formElementItem.formElement.id)"
|
||||
>
|
||||
{{ commentCounts?.[formElementItem.formElement.id] ?? 0 }}
|
||||
</UButton>
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'transition-opacity duration-200',
|
||||
openDropdownId === getElementKey(formElementItem.formElement, formElementItem.indexInSubsection)
|
||||
? 'opacity-100'
|
||||
: 'opacity-100 lg:opacity-0 lg:group-hover:opacity-100 lg:focus-within:opacity-100'
|
||||
]"
|
||||
>
|
||||
<UButton icon="i-lucide-ellipsis-vertical" color="neutral" variant="ghost" />
|
||||
</UDropdownMenu>
|
||||
<UDropdownMenu
|
||||
:items="
|
||||
getDropdownItems(
|
||||
formElementItem.formElement,
|
||||
getElementKey(formElementItem.formElement, formElementItem.indexInSubsection),
|
||||
formElementItem.indexInSubsection
|
||||
)
|
||||
"
|
||||
:content="{ align: 'end' }"
|
||||
@update:open="
|
||||
(isOpen: boolean) =>
|
||||
handleDropdownToggle(
|
||||
getElementKey(formElementItem.formElement, formElementItem.indexInSubsection),
|
||||
isOpen
|
||||
)
|
||||
"
|
||||
>
|
||||
<UButton icon="i-lucide-ellipsis-vertical" color="neutral" variant="ghost" />
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<USeparator v-if="visibleIndex < visibleFormElements.length - 1" />
|
||||
@@ -89,14 +112,18 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const commentStore = useCommentStore()
|
||||
const { load: loadComments } = commentStore
|
||||
const { comments } = storeToRefs(commentStore)
|
||||
const logger = useLogger().withTag('FormEngine')
|
||||
const { loadInitial: loadCommentsInitial } = commentStore
|
||||
const { commentsByApplicationFormId, countsByApplicationFormId } = storeToRefs(commentStore)
|
||||
|
||||
if (props.applicationFormId) {
|
||||
logger.debug('Loading comments for application form:', props.applicationFormId)
|
||||
await loadComments(props.applicationFormId)
|
||||
}
|
||||
const comments = computed(() => {
|
||||
if (!props.applicationFormId) return {}
|
||||
return commentsByApplicationFormId.value[props.applicationFormId] ?? {}
|
||||
})
|
||||
|
||||
const commentCounts = computed(() => {
|
||||
if (!props.applicationFormId) return {}
|
||||
return countsByApplicationFormId.value[props.applicationFormId] ?? {}
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const activeFormElement = ref('')
|
||||
@@ -189,12 +216,15 @@ function updateFormOptions(formOptions: FormOptionDto[], target: VisibleFormElem
|
||||
emit('update:modelValue', updatedModelValue)
|
||||
}
|
||||
|
||||
function toggleComments(formElementId: string) {
|
||||
async function toggleComments(formElementId: string) {
|
||||
if (activeFormElement.value === formElementId) {
|
||||
activeFormElement.value = ''
|
||||
return
|
||||
}
|
||||
activeFormElement.value = formElementId
|
||||
if (props.applicationFormId) {
|
||||
await loadCommentsInitial(props.applicationFormId, formElementId)
|
||||
}
|
||||
emit('click:comments', formElementId)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,67 +1,356 @@
|
||||
<template>
|
||||
<template v-if="comments && comments.length > 0">
|
||||
<UChatMessages :auto-scroll="false" :should-scroll-to-bottom="false">
|
||||
<UChatMessage
|
||||
v-for="comment in comments"
|
||||
:id="comment.id"
|
||||
:key="comment.id"
|
||||
:avatar="{ icon: 'i-lucide-bot' }"
|
||||
:content="comment.message"
|
||||
role="user"
|
||||
:parts="[{ type: 'text', text: comment.message }]"
|
||||
:side="isCommentByUser(comment) ? 'right' : 'left'"
|
||||
variant="subtle"
|
||||
:actions="createChatMessageActions(comment)"
|
||||
>
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<UAvatar icon="i-lucide-bot" />
|
||||
<p class="text-sm">{{ comment.createdBy.name }}</p>
|
||||
<div class="mt-4 lg:mt-6">
|
||||
<UCard variant="subtle" :ui="{ body: 'p-4 sm:p-5', header: 'p-4 sm:p-5', footer: 'p-4 sm:p-5' }">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-medium text-highlighted">
|
||||
{{ $t('comments.title') }}
|
||||
</p>
|
||||
<p class="text-xs text-muted">
|
||||
{{ $t('comments.count', { count: commentCount }) }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</UChatMessage>
|
||||
</UChatMessages>
|
||||
</template>
|
||||
<UTextarea v-model="commentTextAreaValue" class="w-full" />
|
||||
<UButton v-if="!isEditingComment" class="my-3 lg:my-4" @click="submitComment(formElementId)">
|
||||
{{ $t('comments.submit') }}
|
||||
</UButton>
|
||||
<UButton v-if="isEditingComment" class="my-3 lg:my-4" @click="updateEditComment"> {{ $t('comments.edit') }} </UButton>
|
||||
<UButton v-if="isEditingComment" class="my-3 lg:my-4" @click="cancelEditComment"> {{ $t('common.cancel') }} </UButton>
|
||||
<UButton color="neutral" variant="ghost" size="sm" icon="i-lucide-x" @click="$emit('close')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="comments && comments.length > 0" class="relative">
|
||||
<UProgress
|
||||
v-if="isLoadingMore"
|
||||
indeterminate
|
||||
size="xs"
|
||||
class="absolute top-0 inset-x-0 z-10"
|
||||
:ui="{ base: 'bg-default' }"
|
||||
/>
|
||||
|
||||
<UScrollArea ref="scrollAreaRef" class="max-h-96" :ui="{ viewport: 'space-y-4 pe-2' }">
|
||||
<div v-for="comment in comments" :key="comment.id" class="space-y-2">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<UAvatar icon="i-lucide-user" size="2xs" />
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm text-highlighted truncate">
|
||||
{{ comment.createdBy.name }}
|
||||
</p>
|
||||
<p class="text-xs text-muted">
|
||||
{{ comment.createdAt ? formatDate(comment.createdAt) : '-' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<UDropdownMenu
|
||||
v-if="isCommentByUser(comment)"
|
||||
:items="[createCommentActions(comment)]"
|
||||
:content="{ align: 'end' }"
|
||||
>
|
||||
<UButton icon="i-lucide-ellipsis" color="neutral" variant="ghost" size="xs" square />
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md ring-1 ring-inset ring-default bg-default">
|
||||
<!-- Edit comment form -->
|
||||
<template v-if="editingCommentId === comment.id">
|
||||
<UEditor
|
||||
v-slot="{ editor }"
|
||||
v-model="editingCommentEditorContent"
|
||||
content-type="json"
|
||||
:editable="true"
|
||||
:ui="{
|
||||
root: 'bg-transparent',
|
||||
content: 'bg-transparent',
|
||||
base: 'min-h-[120px] p-3 bg-transparent'
|
||||
}"
|
||||
>
|
||||
<UEditorToolbar
|
||||
:editor="editor"
|
||||
:items="toolbarItems"
|
||||
class="border-b border-default px-3 py-2 bg-default/50 overflow-x-auto"
|
||||
/>
|
||||
</UEditor>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-2 sm:justify-end p-3 border-t border-default">
|
||||
<UButton color="neutral" variant="outline" @click="cancelEditComment">
|
||||
{{ $t('common.cancel') }}
|
||||
</UButton>
|
||||
<UButton @click="saveEditedComment(comment)">
|
||||
{{ $t('comments.saveChanges') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Display comment content when not editing -->
|
||||
<UEditor
|
||||
v-else
|
||||
:key="`${comment.id}:${comment.modifiedAt?.toISOString?.() ?? ''}`"
|
||||
:model-value="getCommentEditorModelValue(comment)"
|
||||
content-type="json"
|
||||
:editable="false"
|
||||
:ui="{
|
||||
root: 'bg-transparent',
|
||||
content: 'bg-transparent',
|
||||
base: 'p-3 bg-transparent'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</UScrollArea>
|
||||
</div>
|
||||
|
||||
<UEmpty v-else :title="$t('comments.empty')" icon="i-lucide-message-square" class="py-6" />
|
||||
|
||||
<template #footer>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md ring-1 ring-inset ring-default bg-default">
|
||||
<UEditor
|
||||
v-slot="{ editor }"
|
||||
v-model="newCommentEditorContent"
|
||||
content-type="json"
|
||||
:editable="true"
|
||||
:placeholder="$t('comments.placeholder')"
|
||||
:ui="{
|
||||
root: 'bg-transparent',
|
||||
content: 'bg-transparent',
|
||||
base: 'min-h-[120px] p-3 bg-transparent'
|
||||
}"
|
||||
>
|
||||
<UEditorToolbar
|
||||
:editor="editor"
|
||||
:items="toolbarItems"
|
||||
class="border-b border-default px-3 py-2 bg-transparent overflow-x-auto"
|
||||
/>
|
||||
</UEditor>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-2 sm:justify-end">
|
||||
<UButton @click="submitComment(formElementId)">
|
||||
{{ $t('comments.submit') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CommentDto } from '~~/.api-client'
|
||||
import { useCommentTextarea } from '~/composables/comment/useCommentTextarea'
|
||||
import type { JSONContent } from '@tiptap/vue-3'
|
||||
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||
import { useInfiniteScroll } from '@vueuse/core'
|
||||
import { useCommentStore } from '~~/stores/useCommentStore'
|
||||
import { useUserStore } from '~~/stores/useUserStore'
|
||||
|
||||
const props = defineProps<{
|
||||
formElementId: string
|
||||
applicationFormId: string
|
||||
comments?: CommentDto[]
|
||||
totalCount?: number
|
||||
}>()
|
||||
|
||||
const commentActions = useCommentTextarea(props.applicationFormId)
|
||||
const {
|
||||
submitComment,
|
||||
updateEditComment,
|
||||
cancelEditComment,
|
||||
editComment,
|
||||
isEditingComment,
|
||||
isCommentByUser,
|
||||
commentTextAreaValue
|
||||
} = commentActions
|
||||
defineEmits<{
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
function createChatMessageActions(comment: CommentDto) {
|
||||
const { t: $t } = useI18n()
|
||||
const chatMessageActions = []
|
||||
const commentStore = useCommentStore()
|
||||
const { loadMore } = commentStore
|
||||
const userStore = useUserStore()
|
||||
const { user } = storeToRefs(userStore)
|
||||
const toast = useToast()
|
||||
const { t: $t } = useI18n()
|
||||
|
||||
if (isCommentByUser(comment)) {
|
||||
chatMessageActions.push({
|
||||
const scrollAreaRef = useTemplateRef('scrollAreaRef')
|
||||
const scrollContainerEl = ref<HTMLElement | null>(null)
|
||||
|
||||
const commentCount = computed(() => props.totalCount ?? props.comments?.length ?? 0)
|
||||
|
||||
const commentCursorSate = computed(
|
||||
() => commentStore.nextCursorByApplicationFormId[props.applicationFormId]?.[props.formElementId]
|
||||
)
|
||||
const canLoadMore = computed(() => commentCursorSate.value?.hasMore === true)
|
||||
const isLoadingMore = computed(() => commentCursorSate.value?.isLoading === true)
|
||||
const newCommentValue = ref<string>('')
|
||||
|
||||
const newCommentEditorContent = computed<JSONContent>({
|
||||
get: () => toEditorJson(newCommentValue.value),
|
||||
set: (newValue) => {
|
||||
newCommentValue.value = JSON.stringify(newValue)
|
||||
}
|
||||
})
|
||||
const editingCommentId = ref<string | null>(null)
|
||||
|
||||
const editingCommentValue = ref<string>('')
|
||||
const editingCommentEditorContent = computed<JSONContent>({
|
||||
get: () => toEditorJson(editingCommentValue.value),
|
||||
set: (newValue) => {
|
||||
editingCommentValue.value = JSON.stringify(newValue)
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => scrollAreaRef.value,
|
||||
async (scrollAreaComponent) => {
|
||||
if (!scrollAreaComponent) {
|
||||
scrollContainerEl.value = null
|
||||
return
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
const rootEl = scrollAreaComponent.$el as HTMLElement | undefined
|
||||
if (!rootEl) return
|
||||
|
||||
scrollContainerEl.value = rootEl
|
||||
|
||||
// Wait another tick for content to be measured, then scroll to bottom
|
||||
await nextTick()
|
||||
// Use requestAnimationFrame to ensure layout is complete
|
||||
requestAnimationFrame(() => {
|
||||
rootEl.scrollTop = rootEl.scrollHeight
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
useInfiniteScroll(
|
||||
scrollContainerEl,
|
||||
async () => {
|
||||
const scrollEl = scrollContainerEl.value
|
||||
if (!scrollEl) return
|
||||
|
||||
const previousScrollHeight = scrollEl.scrollHeight
|
||||
await loadMore(props.applicationFormId, props.formElementId)
|
||||
// Maintain scroll position after prepending older comments
|
||||
await nextTick()
|
||||
const newScrollHeight = scrollEl.scrollHeight
|
||||
scrollEl.scrollTop = newScrollHeight - previousScrollHeight + scrollEl.scrollTop
|
||||
},
|
||||
{
|
||||
direction: 'top',
|
||||
distance: 100,
|
||||
canLoadMore: () => canLoadMore.value && !isLoadingMore.value
|
||||
}
|
||||
)
|
||||
|
||||
async function submitComment(formElementId: string) {
|
||||
if (!newCommentValue.value.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await commentStore.createComment(props.applicationFormId, formElementId, { message: newCommentValue.value })
|
||||
newCommentValue.value = ''
|
||||
toast.add({ title: $t('comments.created'), color: 'success' })
|
||||
} catch {
|
||||
toast.add({ title: $t('comments.createError'), color: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
function startEditComment(comment: CommentDto) {
|
||||
editingCommentId.value = comment.id
|
||||
editingCommentValue.value = comment.message || ''
|
||||
}
|
||||
|
||||
function cancelEditComment() {
|
||||
editingCommentId.value = null
|
||||
editingCommentValue.value = ''
|
||||
}
|
||||
|
||||
async function saveEditedComment(comment: CommentDto) {
|
||||
try {
|
||||
const updatedComment: CommentDto = {
|
||||
...comment,
|
||||
message: editingCommentValue.value,
|
||||
modifiedAt: new Date()
|
||||
}
|
||||
|
||||
await commentStore.updateComment(comment.id, updatedComment)
|
||||
cancelEditComment()
|
||||
toast.add({ title: $t('comments.updated'), color: 'success' })
|
||||
} catch {
|
||||
toast.add({ title: $t('comments.updateError'), color: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
function isCommentByUser(comment: CommentDto) {
|
||||
return comment.createdBy.keycloakId === user.value?.keycloakId
|
||||
}
|
||||
|
||||
function createCommentActions(comment: CommentDto): DropdownMenuItem[] {
|
||||
return [
|
||||
{
|
||||
label: $t('comments.editAction'),
|
||||
icon: 'i-lucide-pencil',
|
||||
onClick: () => editComment(comment)
|
||||
})
|
||||
}
|
||||
return chatMessageActions
|
||||
onClick: () => startEditComment(comment)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function getCommentEditorModelValue(comment: CommentDto): JSONContent {
|
||||
return toEditorJson(comment.message)
|
||||
}
|
||||
|
||||
function toEditorJson(rawValue: string | undefined): JSONContent {
|
||||
const raw = (rawValue ?? '').trim()
|
||||
if (raw) {
|
||||
try {
|
||||
if (raw.startsWith('{')) {
|
||||
return JSON.parse(raw) as JSONContent
|
||||
}
|
||||
} catch {
|
||||
// fall through to plain text
|
||||
}
|
||||
return wrapPlainTextAsDoc(raw)
|
||||
}
|
||||
|
||||
return wrapPlainTextAsDoc('')
|
||||
}
|
||||
|
||||
function wrapPlainTextAsDoc(text: string): JSONContent {
|
||||
return {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: text
|
||||
? [
|
||||
{
|
||||
type: 'text',
|
||||
text
|
||||
}
|
||||
]
|
||||
: []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const toolbarItems = [
|
||||
[
|
||||
{ kind: 'undo', icon: 'i-lucide-undo' },
|
||||
{ kind: 'redo', icon: 'i-lucide-redo' }
|
||||
],
|
||||
[
|
||||
{ kind: 'heading', level: 1, icon: 'i-lucide-heading-1', label: 'H1' },
|
||||
{ kind: 'heading', level: 2, icon: 'i-lucide-heading-2', label: 'H2' },
|
||||
{ kind: 'heading', level: 3, icon: 'i-lucide-heading-3', label: 'H3' }
|
||||
],
|
||||
[
|
||||
{ kind: 'mark', mark: 'bold', icon: 'i-lucide-bold' },
|
||||
{ kind: 'mark', mark: 'italic', icon: 'i-lucide-italic' },
|
||||
{ kind: 'mark', mark: 'underline', icon: 'i-lucide-underline' },
|
||||
{ kind: 'mark', mark: 'strike', icon: 'i-lucide-strikethrough' }
|
||||
],
|
||||
[
|
||||
{ kind: 'bulletList', icon: 'i-lucide-list' },
|
||||
{ kind: 'orderedList', icon: 'i-lucide-list-ordered' }
|
||||
],
|
||||
[
|
||||
{ kind: 'blockquote', icon: 'i-lucide-quote' },
|
||||
{ kind: 'codeBlock', icon: 'i-lucide-code' }
|
||||
],
|
||||
[{ kind: 'link', icon: 'i-lucide-link' }]
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { CommentApi, Configuration, type CommentDto, type CreateCommentDto, type PagedCommentDto } from '~~/.api-client'
|
||||
import {
|
||||
CommentApi,
|
||||
Configuration,
|
||||
type ApplicationFormCommentCountsDto,
|
||||
type CommentDto,
|
||||
type CreateCommentDto,
|
||||
type CursorPagedCommentDto
|
||||
} from '~~/.api-client'
|
||||
import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo'
|
||||
import { wrappedFetchWrap } from '~/utils/wrappedFetch'
|
||||
|
||||
@@ -22,8 +29,24 @@ export function useCommentApi() {
|
||||
return commentApiClient.createComment({ applicationFormId, formElementId, createCommentDto })
|
||||
}
|
||||
|
||||
async function getCommentsByApplicationFormId(applicationFormId: string): Promise<PagedCommentDto> {
|
||||
return commentApiClient.getCommentsByApplicationFormId({ applicationFormId })
|
||||
async function getCommentsByApplicationFormId(
|
||||
applicationFormId: string,
|
||||
formElementId?: string,
|
||||
cursorCreatedAt?: Date,
|
||||
limit: number = 10
|
||||
): Promise<CursorPagedCommentDto> {
|
||||
return commentApiClient.getCommentsByApplicationFormId({
|
||||
applicationFormId,
|
||||
formElementId,
|
||||
cursorCreatedAt,
|
||||
limit
|
||||
})
|
||||
}
|
||||
|
||||
async function getGroupedCommentCountByApplicationFromId(
|
||||
applicationFormId: string
|
||||
): Promise<ApplicationFormCommentCountsDto> {
|
||||
return commentApiClient.getGroupedCommentCountByApplicationFromId({ applicationFormId })
|
||||
}
|
||||
|
||||
async function updateComment(id: string, commentDto: CommentDto): Promise<CommentDto> {
|
||||
@@ -37,6 +60,7 @@ export function useCommentApi() {
|
||||
return {
|
||||
createComment,
|
||||
getCommentsByApplicationFormId,
|
||||
getGroupedCommentCountByApplicationFromId,
|
||||
updateComment,
|
||||
deleteCommentById
|
||||
}
|
||||
|
||||
@@ -57,10 +57,12 @@ import {
|
||||
import type { FormElementId } from '~~/types/formElement'
|
||||
import { useApplicationFormValidator } from '~/composables/useApplicationFormValidator'
|
||||
import { useUserStore } from '~~/stores/useUserStore'
|
||||
import { useCommentStore } from '~~/stores/useCommentStore'
|
||||
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
const { t: $t } = useI18n()
|
||||
const commentStore = useCommentStore()
|
||||
|
||||
definePageMeta({
|
||||
// Prevent whole page from re-rendering when navigating between sections to keep state
|
||||
@@ -74,6 +76,10 @@ const {
|
||||
updateApplicationForm
|
||||
} = await useApplicationFormNavigation(applicationFormId!)
|
||||
|
||||
if (applicationFormId) {
|
||||
await commentStore.loadCounts(applicationFormId)
|
||||
}
|
||||
|
||||
const { updateApplicationForm: updateForm, submitApplicationForm } = useApplicationForm()
|
||||
const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator()
|
||||
const { evaluateVisibility } = useFormElementVisibility()
|
||||
|
||||
@@ -62,6 +62,18 @@
|
||||
<p class="text-xs text-muted mt-1">#{{ index }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<UTooltip
|
||||
v-if="(applicationFormElem.commentCount ?? 0) > 0"
|
||||
:text="$t('comments.count', { count: applicationFormElem.commentCount ?? 0 })"
|
||||
>
|
||||
<UBadge
|
||||
:label="applicationFormElem.commentCount ?? 0"
|
||||
color="neutral"
|
||||
variant="subtle"
|
||||
icon="i-lucide-message-square"
|
||||
size="sm"
|
||||
/>
|
||||
</UTooltip>
|
||||
<UBadge
|
||||
v-if="applicationFormElem.status"
|
||||
:label="applicationFormElem.status"
|
||||
|
||||
Reference in New Issue
Block a user