182 lines
5.3 KiB
Vue
182 lines
5.3 KiB
Vue
<script setup lang="ts">
|
|
import { ref, watch, computed } from 'vue'
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
import { Separator } from '@/components/ui/separator'
|
|
import { useApi } from '@/composables/useApi'
|
|
import { useAuth } from '@/composables/useAuth'
|
|
|
|
type CommentRecord = {
|
|
id: string
|
|
content: string
|
|
author_user_id: string
|
|
created_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
interface Props {
|
|
objectApiName: string
|
|
recordId: string
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
const { api } = useApi()
|
|
const { user } = useAuth()
|
|
|
|
const comments = ref<CommentRecord[]>([])
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const newComment = ref('')
|
|
const saving = ref(false)
|
|
const editingId = ref<string | null>(null)
|
|
const editContent = ref('')
|
|
|
|
const isOwner = (comment: CommentRecord) => comment.author_user_id === user.value?.id
|
|
|
|
const formatDate = (value?: string) => {
|
|
if (!value) return ''
|
|
const date = new Date(value)
|
|
if (Number.isNaN(date.getTime())) return value
|
|
return date.toLocaleString()
|
|
}
|
|
|
|
const canSubmit = computed(() => newComment.value.trim().length > 0 && !saving.value)
|
|
const canSaveEdit = computed(() => editContent.value.trim().length > 0 && !saving.value)
|
|
|
|
const fetchComments = async () => {
|
|
if (!props.objectApiName || !props.recordId) return
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const data = await api.get(`/knowledge/comments/${props.objectApiName}/${props.recordId}`)
|
|
comments.value = Array.isArray(data) ? data : []
|
|
} catch (e: any) {
|
|
error.value = e.message || 'Failed to load comments'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const addComment = async () => {
|
|
if (!canSubmit.value) return
|
|
saving.value = true
|
|
try {
|
|
await api.post('/knowledge/comments', {
|
|
parentObjectApiName: props.objectApiName,
|
|
parentRecordId: props.recordId,
|
|
content: newComment.value.trim(),
|
|
})
|
|
newComment.value = ''
|
|
await fetchComments()
|
|
} catch (e: any) {
|
|
error.value = e.message || 'Failed to add comment'
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
const startEdit = (comment: CommentRecord) => {
|
|
editingId.value = comment.id
|
|
editContent.value = comment.content
|
|
}
|
|
|
|
const cancelEdit = () => {
|
|
editingId.value = null
|
|
editContent.value = ''
|
|
}
|
|
|
|
const saveEdit = async () => {
|
|
if (!editingId.value || !canSaveEdit.value) return
|
|
saving.value = true
|
|
try {
|
|
await api.patch(`/knowledge/comments/${editingId.value}`, {
|
|
content: editContent.value.trim(),
|
|
})
|
|
editingId.value = null
|
|
editContent.value = ''
|
|
await fetchComments()
|
|
} catch (e: any) {
|
|
error.value = e.message || 'Failed to update comment'
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
const deleteComment = async (comment: CommentRecord) => {
|
|
if (!confirm('Delete this comment?')) return
|
|
saving.value = true
|
|
try {
|
|
await api.delete(`/knowledge/comments/${comment.id}`)
|
|
await fetchComments()
|
|
} catch (e: any) {
|
|
error.value = e.message || 'Failed to delete comment'
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => [props.objectApiName, props.recordId],
|
|
() => {
|
|
fetchComments()
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Comments</CardTitle>
|
|
</CardHeader>
|
|
<CardContent class="space-y-4">
|
|
<div class="space-y-3">
|
|
<Textarea
|
|
v-model="newComment"
|
|
placeholder="Add a comment..."
|
|
:disabled="saving"
|
|
class="min-h-[96px]"
|
|
/>
|
|
<div class="flex items-center justify-between">
|
|
<p class="text-sm text-muted-foreground" v-if="error">{{ error }}</p>
|
|
<Button size="sm" :disabled="!canSubmit" @click="addComment">
|
|
Add Comment
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div v-if="loading" class="text-sm text-muted-foreground">Loading comments...</div>
|
|
<div v-else-if="comments.length === 0" class="text-sm text-muted-foreground">
|
|
No comments yet.
|
|
</div>
|
|
<div v-else class="space-y-4">
|
|
<div v-for="comment in comments" :key="comment.id" class="rounded-lg border p-4 space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<div class="text-xs text-muted-foreground">
|
|
<span>Author: {{ comment.author_user_id }}</span>
|
|
<span class="mx-2">•</span>
|
|
<span>{{ formatDate(comment.created_at) }}</span>
|
|
</div>
|
|
<div class="flex items-center gap-2" v-if="isOwner(comment)">
|
|
<Button variant="ghost" size="sm" @click="startEdit(comment)">Edit</Button>
|
|
<Button variant="ghost" size="sm" @click="deleteComment(comment)">Delete</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="editingId === comment.id" class="space-y-2">
|
|
<Textarea v-model="editContent" :disabled="saving" class="min-h-[80px]" />
|
|
<div class="flex items-center gap-2">
|
|
<Button size="sm" :disabled="!canSaveEdit" @click="saveEdit">Save</Button>
|
|
<Button variant="ghost" size="sm" @click="cancelEdit">Cancel</Button>
|
|
</div>
|
|
</div>
|
|
<p v-else class="text-sm whitespace-pre-line">{{ comment.content }}</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</template>
|