feat(frontend,backend): Create and load comments
This commit is contained in:
@@ -1,30 +1,79 @@
|
||||
<template>
|
||||
<template v-for="(formElement, index) in props.modelValue" :key="formElement.id">
|
||||
<UFormField>
|
||||
<component
|
||||
:is="getResolvedComponent(formElement)"
|
||||
:form-options="formElement.options"
|
||||
:disabled="props.disabled"
|
||||
@update:form-options="updateFormOptions($event, index)"
|
||||
/>
|
||||
</UFormField>
|
||||
<div class="group py-3 lg:py-4">
|
||||
<div class="flex justify-between">
|
||||
<component
|
||||
:is="getResolvedComponent(formElement)"
|
||||
:form-options="formElement.options"
|
||||
:disabled="props.disabled"
|
||||
@update:form-options="updateFormOptions($event, index)"
|
||||
/>
|
||||
<UIcon
|
||||
name="i-lucide-message-square-more"
|
||||
class="size-5 cursor-pointer hidden group-hover:block"
|
||||
@click="toggleComments(formElement.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="applicationFormId && activeFormElement === formElement.id">
|
||||
<template v-if="comments?.[formElement.id] && comments[formElement.id].length > 0">
|
||||
<UChatMessages :auto-scroll="false" :should-scroll-to-bottom="false">
|
||||
<UChatMessage
|
||||
v-for="comment in comments[formElement.id]"
|
||||
:id="comment.id"
|
||||
:key="comment.id"
|
||||
:avatar="{ icon: 'i-lucide-bot' }"
|
||||
:content="comment.message"
|
||||
role="user"
|
||||
:side="comment.createdBy.id === userDto.id ? 'right' : 'left'"
|
||||
variant="subtle"
|
||||
>
|
||||
<template #leading="{ avatar }">
|
||||
<div class="flex flex-col">
|
||||
<UAvatar icon="i-lucide-bot" />
|
||||
<p class="text-sm">{{ comment.createdBy.name }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</UChatMessage>
|
||||
</UChatMessages>
|
||||
</template>
|
||||
<UTextarea v-model="commentTextAreaValue" class="w-full" />
|
||||
<UButton class="my-3 lg:my-4" @click="submitComment(applicationFormId, formElement.id, commentTextAreaValue)">
|
||||
Submit
|
||||
</UButton>
|
||||
</template>
|
||||
<USeparator />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormElementDto, FormOptionDto } from '~/.api-client'
|
||||
import type { CreateCommentDto, FormElementDto, FormOptionDto } from '~/.api-client'
|
||||
import { resolveComponent } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: FormElementDto[]
|
||||
applicationFormId?: string
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: FormElementDto[]): void
|
||||
(e: 'update:modelValue', formElementDto: FormElementDto[]): void
|
||||
(e: 'click:comments', formElementId: string): void
|
||||
}>()
|
||||
|
||||
const commentStore = useCommentStore()
|
||||
const { load: loadComments, createComment } = commentStore
|
||||
const { comments } = storeToRefs(commentStore)
|
||||
const { userDto } = useAuth()
|
||||
|
||||
if (props.applicationFormId) {
|
||||
console.log('Loading comments for application form:', props.applicationFormId)
|
||||
await loadComments(props.applicationFormId)
|
||||
}
|
||||
|
||||
const activeFormElement = ref('')
|
||||
const commentTextAreaValue = ref('')
|
||||
|
||||
// TODO: Lazy loading?
|
||||
function getResolvedComponent(formElement: FormElementDto) {
|
||||
switch (formElement.type) {
|
||||
@@ -48,4 +97,27 @@ function updateFormOptions(formOptions: FormOptionDto[], formElementIndex: numbe
|
||||
updatedModelValue[formElementIndex] = { ...updatedModelValue[formElementIndex], options: formOptions }
|
||||
emit('update:modelValue', updatedModelValue)
|
||||
}
|
||||
|
||||
function toggleComments(formElementId: string) {
|
||||
if (activeFormElement.value === formElementId) {
|
||||
activeFormElement.value = ''
|
||||
return
|
||||
}
|
||||
activeFormElement.value = formElementId
|
||||
emit('click:comments', formElementId)
|
||||
}
|
||||
|
||||
async function submitComment(applicationFormId: string, formElementId: string, newComment: string) {
|
||||
const newCommentDto: CreateCommentDto = {
|
||||
message: newComment,
|
||||
createdBy: userDto.value
|
||||
}
|
||||
try {
|
||||
await createComment(applicationFormId, formElementId, newCommentDto)
|
||||
commentTextAreaValue.value = ''
|
||||
useToast().add({ title: 'Comment created successfully', color: 'success' })
|
||||
} catch {
|
||||
useToast().add({ title: 'Error creating comment', color: 'error' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
} from '~/.api-client'
|
||||
import { useApplicationFormApi } from './useApplicationFormApi'
|
||||
|
||||
const currentApplicationForm: Ref<ApplicationFormDto | undefined> = ref()
|
||||
|
||||
export function useApplicationForm() {
|
||||
const applicationFormApi = useApplicationFormApi()
|
||||
|
||||
@@ -15,8 +13,7 @@ export function useApplicationForm() {
|
||||
createApplicationFormDto: CreateApplicationFormDto
|
||||
): Promise<ApplicationFormDto> {
|
||||
try {
|
||||
currentApplicationForm.value = await applicationFormApi.createApplicationForm(createApplicationFormDto)
|
||||
return currentApplicationForm.value
|
||||
return await applicationFormApi.createApplicationForm(createApplicationFormDto)
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ResponseError) {
|
||||
console.error('Failed creating application form:', e.response)
|
||||
@@ -62,8 +59,7 @@ export function useApplicationForm() {
|
||||
}
|
||||
|
||||
try {
|
||||
currentApplicationForm.value = await applicationFormApi.updateApplicationForm(id, applicationFormDto)
|
||||
return currentApplicationForm.value
|
||||
return await applicationFormApi.updateApplicationForm(id, applicationFormDto)
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ResponseError) {
|
||||
console.error(`Failed updating application form with ID ${id}:`, e.response)
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { ApplicationFormApi, Configuration } from '../../.api-client'
|
||||
import type { CreateApplicationFormDto, ApplicationFormDto, PagedApplicationFormDto } from '~/.api-client'
|
||||
import {
|
||||
ApplicationFormApi,
|
||||
Configuration,
|
||||
type CreateApplicationFormDto,
|
||||
type ApplicationFormDto,
|
||||
type PagedApplicationFormDto
|
||||
} from '~/.api-client'
|
||||
import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo'
|
||||
|
||||
export function useApplicationFormApi() {
|
||||
|
||||
73
legalconsenthub/composables/comment/useComment.ts
Normal file
73
legalconsenthub/composables/comment/useComment.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { type CreateCommentDto, type CommentDto, type PagedCommentDto, ResponseError } from '~/.api-client'
|
||||
import { useCommentApi } from './useCommentApi'
|
||||
|
||||
export function useComment() {
|
||||
const commentApi = useCommentApi()
|
||||
|
||||
async function createComment(
|
||||
applicationFormId: string,
|
||||
formElementId: string,
|
||||
createCommentDto: CreateCommentDto
|
||||
): Promise<CommentDto> {
|
||||
try {
|
||||
return await commentApi.createComment(applicationFormId, formElementId, createCommentDto)
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ResponseError) {
|
||||
console.error('Failed creating comment:', e.response)
|
||||
} else {
|
||||
console.error('Failed creating comment:', e)
|
||||
}
|
||||
return Promise.reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function getCommentsByApplicationFormId(applicationFormId: string): Promise<PagedCommentDto> {
|
||||
try {
|
||||
return await commentApi.getCommentsByApplicationFormId(applicationFormId)
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ResponseError) {
|
||||
console.error('Failed retrieving comments:', e.response)
|
||||
} else {
|
||||
console.error('Failed retrieving comments:', e)
|
||||
}
|
||||
return Promise.reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateComment(id?: string, commentDto?: CommentDto): Promise<CommentDto> {
|
||||
if (!id || !commentDto) {
|
||||
return Promise.reject(new Error('ID or comment DTO missing'))
|
||||
}
|
||||
|
||||
try {
|
||||
return await commentApi.updateComment(id, commentDto)
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ResponseError) {
|
||||
console.error(`Failed updating comment with ID ${id}:`, e.response)
|
||||
} else {
|
||||
console.error(`Failed updating comment with ID ${id}:`, e)
|
||||
}
|
||||
return Promise.reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCommentById(id: string): Promise<void> {
|
||||
try {
|
||||
return await commentApi.deleteCommentById(id)
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ResponseError) {
|
||||
console.error(`Failed deleting comment with ID ${id}:`, e.response)
|
||||
} else {
|
||||
console.error(`Failed deleting comment with ID ${id}:`, e)
|
||||
}
|
||||
return Promise.reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createComment,
|
||||
getCommentsByApplicationFormId,
|
||||
updateComment,
|
||||
deleteCommentById
|
||||
}
|
||||
}
|
||||
43
legalconsenthub/composables/comment/useCommentApi.ts
Normal file
43
legalconsenthub/composables/comment/useCommentApi.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { CommentApi, Configuration, type CommentDto, type CreateCommentDto, type PagedCommentDto } from '~/.api-client'
|
||||
import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo'
|
||||
|
||||
export function useCommentApi() {
|
||||
const appBaseUrl = useRuntimeConfig().app.baseURL
|
||||
const { serverApiBaseUrl, serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public
|
||||
const { jwt } = useAuth()
|
||||
|
||||
const basePath = withoutTrailingSlash(
|
||||
cleanDoubleSlashes(import.meta.client ? appBaseUrl + clientProxyBasePath : serverApiBaseUrl + serverApiBasePath)
|
||||
)
|
||||
|
||||
const commentApiClient = new CommentApi(
|
||||
new Configuration({ basePath, headers: { Authorization: jwt.value ? `Bearer ${jwt.value}` : '' } })
|
||||
)
|
||||
|
||||
async function createComment(
|
||||
applicationFormId: string,
|
||||
formElementId: string,
|
||||
createCommentDto: CreateCommentDto
|
||||
): Promise<CommentDto> {
|
||||
return commentApiClient.createComment({ applicationFormId, formElementId, createCommentDto })
|
||||
}
|
||||
|
||||
async function getCommentsByApplicationFormId(applicationFormId: string): Promise<PagedCommentDto> {
|
||||
return commentApiClient.getCommentsByApplicationFormId({ applicationFormId })
|
||||
}
|
||||
|
||||
async function updateComment(id: string, commentDto: CommentDto): Promise<CommentDto> {
|
||||
return commentApiClient.updateComment({ id, commentDto })
|
||||
}
|
||||
|
||||
async function deleteCommentById(id: string): Promise<void> {
|
||||
return commentApiClient.deleteComment({ id })
|
||||
}
|
||||
|
||||
return {
|
||||
createComment,
|
||||
getCommentsByApplicationFormId,
|
||||
updateComment,
|
||||
deleteCommentById
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { createAuthClient } from 'better-auth/client'
|
||||
import type { InferSessionFromClient, InferUserFromClient, ClientOptions } from 'better-auth/client'
|
||||
import { organizationClient, jwtClient } from 'better-auth/client/plugins'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
import type { UserDto } from '~/.api-client'
|
||||
|
||||
interface RuntimeAuthConfig {
|
||||
redirectUserTo: RouteLocationRaw | string
|
||||
@@ -103,9 +104,15 @@ export function useAuth() {
|
||||
return res
|
||||
}
|
||||
|
||||
const userDto = computed<UserDto>(() => ({
|
||||
id: user.value?.id ?? '',
|
||||
name: user.value?.name ?? 'Unknown'
|
||||
}))
|
||||
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
userDto,
|
||||
loggedIn: computed(() => !!session.value),
|
||||
signIn: client.signIn,
|
||||
signUp: client.signUp,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"migrate:betterauth": "pnpm dlx @better-auth/cli migrate --config server/utils/auth.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/ui-pro": "3.0.1",
|
||||
"@nuxt/ui-pro": "3.1.1",
|
||||
"@pinia/nuxt": "0.10.1",
|
||||
"better-auth": "1.1.16",
|
||||
"better-sqlite3": "11.8.1",
|
||||
|
||||
@@ -19,13 +19,17 @@
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="flex flex-col gap-4 sm:gap-6 lg:gap-12 w-full lg:max-w-4xl mx-auto">
|
||||
<UPageCard variant="subtle">
|
||||
<UForm class="space-y-4" :state="{}" @submit="onSubmit">
|
||||
<FormEngine v-if="applicationForm" v-model="applicationForm.formElements" :disabled="isReadOnly" />
|
||||
<UButton type="submit" :disabled="isReadOnly">Submit</UButton>
|
||||
</UForm>
|
||||
</UPageCard>
|
||||
<div class="flex flex-col w-full lg:max-w-4xl mx-auto">
|
||||
<UCard variant="subtle">
|
||||
<FormEngine
|
||||
v-if="applicationForm"
|
||||
v-model="applicationForm.formElements"
|
||||
:application-form-id="applicationForm.id"
|
||||
:disabled="isReadOnly"
|
||||
@click:comments="openComments"
|
||||
/>
|
||||
<UButton :disabled="isReadOnly" class="my-3 lg:my-4" @click="onSubmit">Submit</UButton>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
@@ -71,4 +75,8 @@ async function onSubmit() {
|
||||
await navigateTo('/')
|
||||
}
|
||||
}
|
||||
|
||||
function openComments(formElementId: string) {
|
||||
console.log('open comments for', formElementId)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -36,14 +36,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ComplianceStatus, type PagedApplicationFormDto, type UserDto } from '~/.api-client'
|
||||
import { ComplianceStatus, type PagedApplicationFormDto } from '~/.api-client'
|
||||
import { useApplicationFormValidator } from '~/composables/useApplicationFormValidator'
|
||||
import type { FormElementId } from '~/types/FormElement'
|
||||
|
||||
const { getAllApplicationFormTemplates } = useApplicationFormTemplate()
|
||||
const { createApplicationForm } = useApplicationForm()
|
||||
const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator()
|
||||
const { user, selectedOrganization } = useAuth()
|
||||
const { userDto, selectedOrganization } = useAuth()
|
||||
|
||||
const { data } = await useAsyncData<PagedApplicationFormDto>(async () => {
|
||||
return await getAllApplicationFormTemplates()
|
||||
@@ -93,12 +93,8 @@ const ampelStatusEmoji = computed(() => {
|
||||
|
||||
async function onSubmit() {
|
||||
if (applicationFormTemplate.value) {
|
||||
const userDto: UserDto = {
|
||||
id: user.value?.id ?? '',
|
||||
name: user.value?.name ?? 'Unknown'
|
||||
}
|
||||
applicationFormTemplate.value.createdBy = userDto
|
||||
applicationFormTemplate.value.lastModifiedBy = userDto
|
||||
applicationFormTemplate.value.createdBy = userDto.value
|
||||
applicationFormTemplate.value.lastModifiedBy = userDto.value
|
||||
applicationFormTemplate.value.organizationId = selectedOrganization.value?.id ?? ''
|
||||
|
||||
await createApplicationForm(applicationFormTemplate.value)
|
||||
|
||||
1214
legalconsenthub/pnpm-lock.yaml
generated
1214
legalconsenthub/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
98
legalconsenthub/stores/useCommentStore.ts
Normal file
98
legalconsenthub/stores/useCommentStore.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { type CreateCommentDto, type CommentDto, ResponseError } from '~/.api-client'
|
||||
import { useCommentApi } from '~/composables/comment/useCommentApi'
|
||||
|
||||
export const useCommentStore = defineStore('comment', () => {
|
||||
type FormElementId = string
|
||||
const commentApi = useCommentApi()
|
||||
const comments = ref<Record<FormElementId, CommentDto[]>>({})
|
||||
const loadedForms = ref(new Set<string>())
|
||||
|
||||
async function load(applicationFormId: string) {
|
||||
if (loadedForms.value.has(applicationFormId)) return
|
||||
const { data, error } = await useAsyncData(`comments:${applicationFormId}`, () =>
|
||||
commentApi.getCommentsByApplicationFormId(applicationFormId)
|
||||
)
|
||||
if (error.value) {
|
||||
console.error('Failed loading comments:', error.value)
|
||||
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)
|
||||
}
|
||||
|
||||
async function createComment(
|
||||
applicationFormId: string,
|
||||
formElementId: string,
|
||||
createCommentDto: CreateCommentDto
|
||||
): Promise<CommentDto> {
|
||||
try {
|
||||
const newComment = await commentApi.createComment(applicationFormId, formElementId, createCommentDto)
|
||||
if (!comments.value[formElementId]) {
|
||||
comments.value[formElementId] = []
|
||||
}
|
||||
comments.value[formElementId].push(newComment)
|
||||
return newComment
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ResponseError) {
|
||||
console.error('Failed creating comment:', e.response)
|
||||
} else {
|
||||
console.error('Failed creating comment:', e)
|
||||
}
|
||||
return Promise.reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateComment(id?: string, commentDto?: CommentDto): Promise<CommentDto> {
|
||||
if (!id || !commentDto) {
|
||||
return Promise.reject(new Error('ID or comment DTO missing'))
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedComment = await commentApi.updateComment(id, commentDto)
|
||||
const formElementId = updatedComment.formElementId
|
||||
const index = comments.value?.[formElementId]?.findIndex((comment) => comment.id === id) ?? -1
|
||||
if (index !== -1 && comments.value?.[formElementId][index]) {
|
||||
comments.value[formElementId][index] = updatedComment
|
||||
}
|
||||
return updatedComment
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ResponseError) {
|
||||
console.error(`Failed updating comment with ID ${id}:`, e.response)
|
||||
} else {
|
||||
console.error(`Failed updating comment with ID ${id}:`, e)
|
||||
}
|
||||
return Promise.reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCommentById(id: string): Promise<void> {
|
||||
try {
|
||||
await commentApi.deleteCommentById(id)
|
||||
for (const formElementId in comments.value) {
|
||||
const index = comments.value[formElementId].findIndex((comment) => comment.id === id)
|
||||
if (index !== -1) {
|
||||
comments.value[formElementId].splice(index, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ResponseError) {
|
||||
console.error(`Failed deleting comment with ID ${id}:`, e.response)
|
||||
} else {
|
||||
console.error(`Failed deleting comment with ID ${id}:`, e)
|
||||
}
|
||||
return Promise.reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
return { load, createComment, updateComment, deleteCommentById, comments }
|
||||
})
|
||||
Reference in New Issue
Block a user