WIP - initial UI for comments and semantic links
This commit is contained in:
181
frontend/components/knowledge/RecordCommentsPanel.vue
Normal file
181
frontend/components/knowledge/RecordCommentsPanel.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user