多维表格
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

376 lines
12 KiB

<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import type { AuditType } from 'nocodb-sdk'
import { timeAgo } from 'nocodb-sdk'
import { Icon } from '@iconify/vue'
import { ref, useExpandedFormStoreOrThrow, useGlobal, useRoles, watch } from '#imports'
const props = defineProps<{
loading: boolean
}>()
const { loadCommentsAndLogs, commentsAndLogs, saveComment: _saveComment, comment, updateComment } = useExpandedFormStoreOrThrow()
const { isExpandedFormCommentMode } = storeToRefs(useConfigStore())
const commentsWrapperEl = ref<HTMLDivElement>()
const { user, appInfo } = useGlobal()
const isExpandedFormLoading = computed(() => props.loading)
const tab = ref<'comments' | 'audits'>('comments')
const { isUIAllowed } = useRoles()
const hasEditPermission = computed(() => isUIAllowed('commentEdit'))
const editLog = ref<AuditType>()
const isEditing = ref<boolean>(false)
const isCommentMode = ref(false)
const focusCommentInput: VNodeRef = (el) => {
if (!isExpandedFormLoading.value && (isCommentMode.value || isExpandedFormCommentMode.value) && !isEditing.value) {
if (isExpandedFormCommentMode.value) {
setTimeout(() => {
isExpandedFormCommentMode.value = false
}, 400)
}
return (el as HTMLInputElement)?.focus()
}
return el
}
const focusInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
function onKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
onKeyEsc(event)
} else if (event.key === 'Enter') {
onKeyEnter(event)
}
}
function onKeyEnter(event: KeyboardEvent) {
event.stopImmediatePropagation()
event.preventDefault()
onEditComment()
}
function onKeyEsc(event: KeyboardEvent) {
event.stopImmediatePropagation()
event.preventDefault()
onCancel()
}
async function onEditComment() {
if (!isEditing.value || !editLog.value) return
isCommentMode.value = true
await updateComment(editLog.value.id!, {
description: editLog.value.description,
})
onStopEdit()
}
function onCancel() {
if (!isEditing.value) return
editLog.value = undefined
onStopEdit()
}
function onStopEdit() {
loadCommentsAndLogs()
isEditing.value = false
editLog.value = undefined
}
onKeyStroke('Enter', (event) => {
if (isEditing.value) {
onKeyEnter(event)
}
})
const comments = computed(() => commentsAndLogs.value.filter((log) => log.op_type === 'COMMENT'))
const audits = computed(() => commentsAndLogs.value.filter((log) => log.op_type !== 'COMMENT' && log.details))
function editComment(log: AuditType) {
editLog.value = log
isEditing.value = true
}
const value = computed({
get() {
return editLog.value?.description?.substring(editLog.value?.description?.indexOf(':') + 1) ?? ''
},
set(val) {
if (!editLog.value) return
editLog.value.description = val
},
})
function scrollComments() {
if (commentsWrapperEl.value) commentsWrapperEl.value.scrollTop = commentsWrapperEl.value?.scrollHeight
}
const isSaving = ref(false)
const saveComment = async () => {
if (isSaving.value) return
isCommentMode.value = true
isSaving.value = true
try {
await _saveComment()
scrollComments()
} catch (e) {
console.error(e)
} finally {
isSaving.value = false
}
}
watch(commentsWrapperEl, () => {
scrollComments()
})
const onClickAudit = () => {
if (appInfo.value.ee) return
tab.value = 'audits'
}
</script>
<template>
<div class="flex flex-col h-full w-full">
<div class="h-16 bg-white rounded-t-lg border-gray-200 border-b-1">
<div class="flex flex-row gap-2 m-2 p-1 bg-gray-100 rounded-lg">
<div
v-e="['c:row-expand:comment']"
class="tab flex-1 px-4 py-2 transition-all text-gray-600 cursor-pointer rounded-lg"
:class="{
'bg-white shadow !text-brand-500 !hover:text-brand-500': tab === 'comments' || appInfo.ee,
}"
@click="tab = 'comments'"
>
<div class="tab-title nc-tab">
<MdiMessageOutline class="h-4 w-4 flex-none" />
<span class="<lg:hidden">Comments</span>
</div>
</div>
<NcTooltip v-if="appInfo.ee" class="tab flex-1">
<template #title>
<span class="!text-base"> Coming soon </span>
</template>
<div
v-e="['c:row-expand:audit']"
class="flex-1 px-4 py-2 transition-all text-gray-400 cursor-not-allowed bg-gray-50 rounded-lg"
@click="onClickAudit"
>
<div class="tab-title nc-tab select-none">
<MdiFileDocumentOutline class="h-4 w-4" />
<span class="<lg:hidden">Audits</span>
</div>
</div>
</NcTooltip>
<div
v-else
v-e="['c:row-expand:audit']"
class="tab flex-1 px-4 py-2 transition-all text-gray-600 cursor-pointer rounded-lg"
:class="{
'bg-white shadow !text-brand-500 !hover:text-brand-500': tab === 'audits',
}"
@click="onClickAudit"
>
<div class="tab-title nc-tab">
<MdiFileDocumentOutline class="h-4 w-4" />
<span class="<lg:hidden">Audits</span>
</div>
</div>
</div>
</div>
<div
class="h-[calc(100%-4rem)]"
:class="{
'pb-1': tab !== 'comments' && !appInfo.ee,
}"
>
<div v-if="isExpandedFormLoading" class="flex flex-col h-full">
<GeneralLoader class="!mt-16" size="xlarge" />
</div>
<div v-else-if="tab === 'comments'" class="flex flex-col h-full">
<div v-if="comments.length === 0" class="flex flex-col my-1 text-center justify-center h-full">
<div class="text-center text-3xl text-gray-700">
<GeneralIcon icon="commentHere" />
</div>
<div class="font-medium text-center my-6 text-gray-500">{{ $t('activity.startCommenting') }}</div>
</div>
<div v-else ref="commentsWrapperEl" class="flex flex-col h-full py-2 pl-2 pr-1 space-y-2 nc-scrollbar-md">
<div v-for="log of comments" :key="log.id">
<div class="bg-white rounded-xl group border-1 gap-2 border-gray-200 overflow-hidden">
<div class="flex flex-col p-4 gap-3">
<div class="flex justify-between">
<div class="flex items-center gap-2">
<GeneralUserIcon size="base" :name="log.display_name" :email="log.user" />
<div class="flex flex-col <lg:max-w-22">
<NcTooltip class="truncate max-w-42" show-on-truncate-only>
<template #title>
{{ log.display_name?.trim() || log.user || 'Shared source' }}
</template>
<span
class="text-ellipsis overflow-hidden font-bold"
:style="{
wordBreak: 'keep-all',
whiteSpace: 'nowrap',
display: 'inline',
}"
>
{{ log.display_name?.trim() || log.user || 'Shared source' }}
</span>
</NcTooltip>
<div v-if="log.id !== editLog?.id" class="text-xs font-medium text-gray-500">
{{ log.created_at !== log.updated_at ? `Edited ${timeAgo(log.updated_at)}` : timeAgo(log.created_at) }}
</div>
</div>
</div>
<NcButton
v-if="log.user === user!.email && !editLog"
v-e="['c:row-expand:comment:edit']"
type="secondary"
class="!px-2 opacity-0 group-hover:opacity-100 transition-all"
size="sm"
@click="editComment(log)"
>
<Icon class="iconify text-gray-800" icon="lucide:pen" />
</NcButton>
</div>
<textarea
v-if="log.id === editLog?.id"
:ref="focusInput"
v-model="value"
rows="6"
class="px-2 py-1 rounded-lg border-none nc-scrollbar-md bg-white outline-gray-200"
@keydown.stop="onKeyDown($event)"
/>
<div v-else class="text-sm text-gray-700">
{{ log.description.substring(log.description.indexOf(':') + 1) }}
</div>
<div v-if="log.id === editLog?.id" class="flex justify-end gap-1">
<NcButton type="secondary" size="sm" @click="onCancel"> Cancel </NcButton>
<NcButton v-e="['a:row-expand:comment:save']" size="sm" @click="onEditComment"> Save </NcButton>
</div>
</div>
</div>
</div>
</div>
<div v-if="hasEditPermission" class="p-2 bg-gray-50 gap-2 flex">
<div class="h-14 flex flex-row w-full bg-white py-2.75 px-1.5 items-center rounded-xl border-1 border-gray-200">
<GeneralUserIcon size="base" class="!w-10" :email="user?.email" :name="user?.display_name" />
<a-input
:ref="focusCommentInput"
v-model:value="comment"
class="!rounded-lg border-1 bg-white !px-2.5 !py-2 !border-gray-200 nc-comment-box !outline-none"
placeholder="Start typing..."
data-testid="expanded-form-comment-input"
:bordered="false"
:disabled="isSaving"
@keyup.enter.prevent="saveComment"
>
</a-input>
<NcButton
v-e="['a:row-expand:comment:save']"
size="medium"
class="!w-8"
:loading="isSaving"
:disabled="!isSaving && !comment.length"
:icon-only="isSaving"
@click="saveComment"
>
<GeneralIcon v-if="!isSaving" icon="send" />
</NcButton>
</div>
</div>
</div>
<div v-if="tab === 'audits'" ref="commentsWrapperEl" class="flex flex-col h-full nc-scrollbar-md !overflow-y-auto">
<template v-if="audits.length === 0">
<div class="flex flex-col text-center justify-center h-full">
<div class="text-center text-3xl text-gray-600">
<MdiHistory />
</div>
<div class="font-bold text-center my-1 text-gray-600">See changes to this record</div>
</div>
</template>
<div v-for="log of audits" :key="log.id" class="nc-audit-item">
<div class="flex flex-col p-4 gap-3">
<div class="flex justify-between">
<div class="flex items-center gap-2">
<GeneralUserIcon size="base" :email="log.user" :name="log.display_name" />
<div class="flex flex-col">
<NcTooltip class="truncate max-w-50" show-on-truncate-only>
<template #title>
{{ log.display_name?.trim() || log.user || 'Shared source' }}
</template>
<span
class="text-ellipsis overflow-hidden font-bold"
:style="{
wordBreak: 'keep-all',
whiteSpace: 'nowrap',
display: 'inline',
}"
>
{{ log.display_name?.trim() || log.user || 'Shared source' }}
</span>
</NcTooltip>
<div v-if="log.id !== editLog?.id" class="text-xs font-medium text-gray-500">
{{ timeAgo(log.created_at) }}
</div>
</div>
</div>
</div>
<div v-dompurify-html="log.details" class="text-sm font-medium"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.tab {
@apply max-w-1/2;
}
.nc-audit-item {
@apply border-b-1 gap-3 border-gray-200;
}
.nc-audit-item:last-child {
@apply border-b-0;
}
.tab .tab-title {
@apply min-w-0 flex justify-center gap-2 font-semibold items-center;
word-break: 'keep-all';
white-space: 'nowrap';
display: 'inline';
}
.text-decoration-line-through {
text-decoration: line-through;
}
:deep(.red.lighten-4) {
@apply bg-red-100 rounded-md line-through;
}
:deep(.green.lighten-4) {
@apply bg-green-100 rounded-md !mr-3 !leading-6;
}
</style>