feat(#21): Replaced chat comment component with proper cursor-based commenting
This commit is contained in:
@@ -1,47 +1,80 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { type CreateCommentDto, type CommentDto, ResponseError } from '~~/.api-client'
|
||||
import { type CreateCommentDto, type CommentDto, ResponseError, type CursorPagedCommentDto } from '~~/.api-client'
|
||||
import { useCommentApi } from '~/composables/comment/useCommentApi'
|
||||
import { useLogger } from '../app/composables/useLogger'
|
||||
|
||||
type ApplicationFormId = string
|
||||
type FormElementId = string
|
||||
|
||||
export const useCommentStore = defineStore('Comment', () => {
|
||||
type FormElementId = string
|
||||
const nextCursorByApplicationFormId = ref<
|
||||
Record<
|
||||
ApplicationFormId,
|
||||
Record<FormElementId, { nextCursorCreatedAt: Date | null; hasMore: boolean; isLoading: boolean }>
|
||||
>
|
||||
>({})
|
||||
const commentsByApplicationFormId = ref<Record<ApplicationFormId, Record<FormElementId, CommentDto[]>>>({})
|
||||
const countsByApplicationFormId = ref<Record<ApplicationFormId, Record<FormElementId, number>>>({})
|
||||
|
||||
const commentApi = useCommentApi()
|
||||
const comments = ref<Record<FormElementId, CommentDto[]>>({})
|
||||
const loadedForms = ref(new Set<string>())
|
||||
const loadedFormElementComments = ref(new Set<string>())
|
||||
const logger = useLogger().withTag('commentStore')
|
||||
|
||||
async function load(applicationFormId: string) {
|
||||
if (loadedForms.value.has(applicationFormId)) return
|
||||
const { data, error } = await useAsyncData(`comments:${applicationFormId}`, () =>
|
||||
commentApi.getCommentsByApplicationFormId(applicationFormId)
|
||||
)
|
||||
if (error.value) {
|
||||
logger.error('Failed loading comments:', error.value)
|
||||
async function loadInitial(applicationFormId: ApplicationFormId, formElementId: FormElementId) {
|
||||
initializeCursorState(applicationFormId, formElementId)
|
||||
|
||||
const cacheKey = `${applicationFormId}:${formElementId}`
|
||||
if (loadedFormElementComments.value.has(cacheKey)) return
|
||||
|
||||
nextCursorByApplicationFormId.value[applicationFormId]![formElementId]!.isLoading = true
|
||||
try {
|
||||
const page = await commentApi.getCommentsByApplicationFormId(applicationFormId, formElementId, undefined, 10)
|
||||
upsertComments(applicationFormId, formElementId, page, { prepend: false })
|
||||
} catch (e) {
|
||||
nextCursorByApplicationFormId.value[applicationFormId]![formElementId]!.isLoading = false
|
||||
logger.error('Failed loading initial comments:', e)
|
||||
return
|
||||
}
|
||||
comments.value =
|
||||
data.value?.content.reduce((acc: Record<FormElementId, CommentDto[]>, comment: CommentDto) => {
|
||||
const formElementId = comment.formElementId
|
||||
if (!acc[formElementId]) {
|
||||
acc[formElementId] = []
|
||||
}
|
||||
acc[formElementId].push(comment)
|
||||
return acc
|
||||
}, {}) || {}
|
||||
loadedForms.value.add(applicationFormId)
|
||||
loadedFormElementComments.value.add(cacheKey)
|
||||
}
|
||||
|
||||
async function loadMore(applicationFormId: ApplicationFormId, formElementId: FormElementId) {
|
||||
initializeCursorState(applicationFormId, formElementId)
|
||||
const state = nextCursorByApplicationFormId.value[applicationFormId]![formElementId]!
|
||||
if (state.isLoading || !state.hasMore) return
|
||||
|
||||
state.isLoading = true
|
||||
const cursor = state.nextCursorCreatedAt ?? undefined
|
||||
|
||||
try {
|
||||
const page = await commentApi.getCommentsByApplicationFormId(applicationFormId, formElementId, cursor, 10)
|
||||
// Prepend older comments when loading more (scroll up)
|
||||
upsertComments(applicationFormId, formElementId, page, { prepend: true })
|
||||
} catch (e) {
|
||||
state.isLoading = false
|
||||
logger.error('Failed loading more comments:', e)
|
||||
return Promise.reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function createComment(
|
||||
applicationFormId: string,
|
||||
applicationFormId: ApplicationFormId,
|
||||
formElementId: string,
|
||||
createCommentDto: CreateCommentDto
|
||||
): Promise<CommentDto> {
|
||||
try {
|
||||
const newComment = await commentApi.createComment(applicationFormId, formElementId, createCommentDto)
|
||||
if (!comments.value[formElementId]) {
|
||||
comments.value[formElementId] = []
|
||||
const commentsByFormElement = commentsByApplicationFormId.value[applicationFormId] ?? {}
|
||||
if (!commentsByFormElement[formElementId]) {
|
||||
commentsByFormElement[formElementId] = []
|
||||
}
|
||||
comments.value[formElementId].push(newComment)
|
||||
commentsByFormElement[formElementId].push(newComment)
|
||||
commentsByApplicationFormId.value[applicationFormId] = commentsByFormElement
|
||||
|
||||
const currentCounts = countsByApplicationFormId.value[applicationFormId] ?? {}
|
||||
currentCounts[formElementId] = (currentCounts[formElementId] ?? 0) + 1
|
||||
countsByApplicationFormId.value[applicationFormId] = currentCounts
|
||||
|
||||
return newComment
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ResponseError) {
|
||||
@@ -60,14 +93,18 @@ export const useCommentStore = defineStore('Comment', () => {
|
||||
|
||||
try {
|
||||
const updatedComment = await commentApi.updateComment(id, commentDto)
|
||||
const formElementId = updatedComment.formElementId
|
||||
const formElementComments = comments.value?.[formElementId]
|
||||
|
||||
const commentsByFormElement = commentsByApplicationFormId.value[updatedComment.applicationFormId]
|
||||
const formElementComments = commentsByFormElement?.[updatedComment.formElementId]
|
||||
|
||||
// Update the comment in the store
|
||||
if (formElementComments) {
|
||||
const index = formElementComments.findIndex((comment) => comment.id === id)
|
||||
if (index !== -1 && formElementComments[index]) {
|
||||
formElementComments[index] = updatedComment
|
||||
}
|
||||
}
|
||||
|
||||
return updatedComment
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ResponseError) {
|
||||
@@ -82,13 +119,33 @@ export const useCommentStore = defineStore('Comment', () => {
|
||||
async function deleteCommentById(id: string): Promise<void> {
|
||||
try {
|
||||
await commentApi.deleteCommentById(id)
|
||||
for (const formElementId in comments.value) {
|
||||
const formElementComments = comments.value[formElementId]
|
||||
if (formElementComments) {
|
||||
const index = formElementComments.findIndex((comment) => comment.id === id)
|
||||
if (index !== -1) {
|
||||
formElementComments.splice(index, 1)
|
||||
break
|
||||
|
||||
// Remove the comment from the store
|
||||
for (const applicationFormId in commentsByApplicationFormId.value) {
|
||||
const commentsByFormElement = commentsByApplicationFormId.value[applicationFormId]
|
||||
|
||||
if (!commentsByFormElement) continue
|
||||
|
||||
for (const formElementId in commentsByFormElement) {
|
||||
const formElementComments = commentsByFormElement[formElementId]
|
||||
|
||||
if (formElementComments) {
|
||||
const index = formElementComments.findIndex((comment) => comment.id === id)
|
||||
|
||||
if (index !== -1) {
|
||||
// Remove the comment from the array
|
||||
formElementComments.splice(index, 1)
|
||||
const commentCountsByFormElement = countsByApplicationFormId.value[applicationFormId]
|
||||
|
||||
// Decrement the comment count for the form element
|
||||
if (commentCountsByFormElement && commentCountsByFormElement[formElementId] != null) {
|
||||
commentCountsByFormElement[formElementId] = Math.max(
|
||||
0,
|
||||
(commentCountsByFormElement[formElementId] ?? 0) - 1
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,5 +159,67 @@ export const useCommentStore = defineStore('Comment', () => {
|
||||
}
|
||||
}
|
||||
|
||||
return { load, createComment, updateComment, deleteCommentById, comments }
|
||||
function upsertComments(
|
||||
applicationFormId: ApplicationFormId,
|
||||
formElementId: FormElementId,
|
||||
page: CursorPagedCommentDto,
|
||||
options: { prepend: boolean }
|
||||
) {
|
||||
const applicationFormComments = commentsByApplicationFormId.value[applicationFormId] ?? {}
|
||||
const formElementComments = applicationFormComments[formElementId] ?? (applicationFormComments[formElementId] = [])
|
||||
|
||||
const newComments = page.content.filter((newComment) => !formElementComments.some((c) => c.id === newComment.id))
|
||||
|
||||
if (options.prepend) {
|
||||
// Prepend older comments at the beginning (they come in DESC order, so reverse to maintain chronological order)
|
||||
formElementComments.unshift(...newComments.reverse())
|
||||
} else {
|
||||
// Initial load: comments come in DESC order (newest first), reverse for display (oldest at top, newest at bottom)
|
||||
formElementComments.push(...newComments.reverse())
|
||||
}
|
||||
|
||||
commentsByApplicationFormId.value[applicationFormId] = applicationFormComments
|
||||
|
||||
nextCursorByApplicationFormId.value[applicationFormId]![formElementId] = {
|
||||
nextCursorCreatedAt: page.nextCursorCreatedAt ?? null,
|
||||
hasMore: page.hasMore,
|
||||
isLoading: false
|
||||
}
|
||||
}
|
||||
|
||||
function initializeCursorState(applicationFormId: ApplicationFormId, formElementId: FormElementId) {
|
||||
if (!nextCursorByApplicationFormId.value[applicationFormId]) {
|
||||
nextCursorByApplicationFormId.value[applicationFormId] = {}
|
||||
}
|
||||
if (!nextCursorByApplicationFormId.value[applicationFormId]![formElementId]) {
|
||||
nextCursorByApplicationFormId.value[applicationFormId]![formElementId] = {
|
||||
nextCursorCreatedAt: null,
|
||||
hasMore: true,
|
||||
isLoading: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCounts(applicationFormId: ApplicationFormId) {
|
||||
try {
|
||||
const result = await commentApi.getGroupedCommentCountByApplicationFromId(applicationFormId)
|
||||
countsByApplicationFormId.value[applicationFormId] = Object.fromEntries(
|
||||
Object.entries(result.counts ?? {}).map(([formElementId, count]) => [formElementId, Number(count)])
|
||||
)
|
||||
} catch (e) {
|
||||
logger.error('Failed loading comment counts:', e)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loadCounts,
|
||||
loadInitial,
|
||||
loadMore,
|
||||
createComment,
|
||||
updateComment,
|
||||
deleteCommentById,
|
||||
commentsByApplicationFormId,
|
||||
nextCursorByApplicationFormId,
|
||||
countsByApplicationFormId
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user