Files
speckle-server/packages/frontend-2/lib/viewer/composables/commentManagement.ts
T
Kristaps Fabians Geikins 83d8035dc2 chore: upgrade to eslint 9 (#2348)
* root + server

* frontend

* frontend-2

* dui3

* dui3

* tailwind theme

* ui-components

* preview service

* viewer

* viewer-sandbox

* fileimport-service

* webhook service

* objectloader

* shared

* ui-components-nuxt

* WIP full config

* WIP full linter

* eslint projectwide util

* minor fix

* removing redundant ci

* clean up test errors

* fixed prettier formatting

* CI improvements

* TSC lint fix

* 'buildBatch' needs to be async since some batch types (like Text) require it. Removed a disabled liniting rule from ObjLoader

* removed unnecessary void

---------

Co-authored-by: AlexandruPopovici <alexandrupopoviciioan@gmail.com>
2024-06-12 14:38:02 +03:00

236 lines
6.5 KiB
TypeScript

import type { ApolloCache } from '@apollo/client/cache'
import type { JSONContent } from '@tiptap/core'
import { useApolloClient, useSubscription } from '@vue/apollo-composable'
import type { MaybeRef } from '@vueuse/core'
import dayjs from 'dayjs'
import type { Get } from 'type-fest'
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
import type {
CommentContentInput,
CreateCommentReplyInput,
OnViewerCommentsUpdatedSubscription
} from '~~/lib/common/generated/gql/graphql'
import {
convertThrowIntoFetchResult,
getCacheId,
getFirstErrorMessage
} from '~~/lib/common/helpers/graphql'
import {
archiveCommentMutation,
createCommentReplyMutation,
createCommentThreadMutation,
markCommentViewedMutation
} from '~~/lib/viewer/graphql/mutations'
import { onViewerCommentsUpdatedSubscription } from '~~/lib/viewer/graphql/subscriptions'
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
import type { MaybeNullOrUndefined } from '@speckle/shared'
import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
import type { SuccessfullyUploadedFileItem } from '~~/lib/core/api/blobStorage'
import { isValidCommentContentInput } from '~~/lib/viewer/helpers/comments'
import { useStateSerialization } from '~~/lib/viewer/composables/serialization'
export function useViewerCommentUpdateTracking(
params: {
projectId: MaybeRef<string>
resourceIdString: MaybeRef<string>
loadedVersionsOnly?: MaybeRef<MaybeNullOrUndefined<boolean>>
},
handler?: (
data: NonNullable<
Get<OnViewerCommentsUpdatedSubscription, 'projectCommentsUpdated'>
>,
cache: ApolloCache<unknown>
) => void
) {
const apollo = useApolloClient().client
const { onResult: onViewerCommentUpdated } = useSubscription(
onViewerCommentsUpdatedSubscription,
() => ({
target: {
projectId: unref(params.projectId),
resourceIdString: unref(params.resourceIdString),
loadedVersionsOnly: unref(params.loadedVersionsOnly)
}
})
)
onViewerCommentUpdated((res) => {
if (!res.data?.projectCommentsUpdated) return
const event = res.data.projectCommentsUpdated
const cache = apollo.cache
handler?.(event, cache)
})
}
export function useMarkThreadViewed() {
const apollo = useApolloClient().client
const { isLoggedIn } = useActiveUser()
const logger = useLogger()
return async (projectId: string, threadId: string) => {
if (!isLoggedIn.value) return false
const { data, errors } = await apollo
.mutate({
mutation: markCommentViewedMutation,
variables: {
projectId,
threadId
},
update: (cache, { data }) => {
if (!data?.commentMutations.markViewed) return
cache.modify({
id: getCacheId('Comment', threadId),
fields: {
viewedAt: () => dayjs().toISOString()
}
})
}
})
.catch(convertThrowIntoFetchResult)
if (errors) {
logger.error('Marking thread as viewed failed', errors)
}
return !!data?.commentMutations.markViewed
}
}
export type CommentEditorValue = {
doc?: JSONContent | null
attachments?: SuccessfullyUploadedFileItem[] | null
}
export function useSubmitComment() {
const {
projectId,
resources: {
request: { resourceIdString }
},
viewer: { instance: viewerInstance }
} = useInjectedViewerState()
const { isLoggedIn } = useActiveUser()
const client = useApolloClient().client
const { triggerNotification } = useGlobalToast()
const { serialize } = useStateSerialization()
return async (content: CommentContentInput) => {
if (!isLoggedIn.value) return null
if (!isValidCommentContentInput(content)) return null
const screenshot = await viewerInstance.screenshot()
const { data, errors } = await client
.mutate({
mutation: createCommentThreadMutation,
variables: {
input: {
projectId: projectId.value,
resourceIdString: resourceIdString.value,
content,
viewerState: serialize({ concreteResourceIdString: true }),
screenshot
}
}
})
.catch(convertThrowIntoFetchResult)
if (data?.commentMutations.create) {
return data.commentMutations.create
}
const errMsg = getFirstErrorMessage(errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Comment creation failed',
description: errMsg
})
return null
}
}
export function useSubmitReply() {
const { isLoggedIn } = useActiveUser()
const client = useApolloClient().client
const { triggerNotification } = useGlobalToast()
return async (input: CreateCommentReplyInput) => {
if (!isLoggedIn.value) return null
if (!isValidCommentContentInput(input.content)) return null
const { data, errors } = await client
.mutate({
mutation: createCommentReplyMutation,
variables: {
input
}
})
.catch(convertThrowIntoFetchResult)
if (data?.commentMutations.reply) {
return data.commentMutations.reply
}
const errMsg = getFirstErrorMessage(errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Reply creation failed',
description: errMsg
})
return null
}
}
export function useArchiveComment() {
const { isLoggedIn } = useActiveUser()
const client = useApolloClient().client
const { triggerNotification } = useGlobalToast()
return async (commentId: string, archived = true) => {
if (!isLoggedIn.value || !commentId) return false
const { data, errors } = await client
.mutate({
mutation: archiveCommentMutation,
variables: {
commentId,
archived
}
})
.catch(convertThrowIntoFetchResult)
if (data?.commentMutations.archive) return true
const errMsg = getFirstErrorMessage(errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Comment archival failed',
description: errMsg
})
return false
}
}
export function useCheckViewerCommentingAccess() {
const {
resources: {
response: { project }
}
} = useInjectedViewerState()
const { activeUser } = useActiveUser()
return computed(() => {
if (!activeUser.value) return false
const hasRole = !!project.value?.role
const allowPublicComments = !!project.value?.allowPublicComments
return hasRole || allowPublicComments
})
}