Browse Source

fix: missing changes (#8724)

Signed-off-by: mertmit <mertmit99@gmail.com>
pull/8728/head
Mert E 5 months ago committed by GitHub
parent
commit
9cb4411366
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 11
      packages/nc-gui/assets/nc-icons/checkFill.svg
  2. 7
      packages/nc-gui/assets/nc-icons/external-link.svg
  3. 5
      packages/nc-gui/assets/nc-icons/strike-through.svg
  4. 4
      packages/nc-gui/components/account/Profile.vue
  5. 14
      packages/nc-gui/components/cell/RichText/LinkOptions.vue
  6. 195
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  7. 89
      packages/nc-gui/components/smartsheet/expanded-form/RichComment.vue
  8. 2
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  9. 11
      packages/nc-gui/helpers/dbTiptapExtensions/links.ts
  10. 4
      packages/nc-gui/utils/iconUtils.ts
  11. 7
      packages/nocodb/src/meta/meta.service.ts
  12. 24
      packages/nocodb/src/models/Comment.ts
  13. 2
      packages/nocodb/src/services/notifications/notifications.service.ts

11
packages/nc-gui/assets/nc-icons/checkFill.svg

@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="check-2" clip-path="url(#clip0_605_57951)">
<path id="Vector" d="M8.00004 14.6663C11.6819 14.6663 14.6667 11.6816 14.6667 7.99967C14.6667 4.31778 11.6819 1.33301 8.00004 1.33301C4.31814 1.33301 1.33337 4.31778 1.33337 7.99967C1.33337 11.6816 4.31814 14.6663 8.00004 14.6663Z" fill="#17803D" stroke="#17803D" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M11.5 5.5L6.6875 10.5L4.5 8.22727" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_605_57951">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 750 B

7
packages/nc-gui/assets/nc-icons/external-link.svg

@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="external-link">
<path id="Vector" d="M12 8.66667V12.6667C12 13.0203 11.8595 13.3594 11.6095 13.6095C11.3594 13.8595 11.0203 14 10.6667 14H3.33333C2.97971 14 2.64057 13.8595 2.39052 13.6095C2.14048 13.3594 2 13.0203 2 12.6667V5.33333C2 4.97971 2.14048 4.64057 2.39052 4.39052C2.64057 4.14048 2.97971 4 3.33333 4H7.33333" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M10 2H14V6" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M6.66669 9.33333L14 2" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 798 B

5
packages/nc-gui/assets/nc-icons/strike-through.svg

@ -1,7 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="strike-through"> <g id="strike-through">
<path id="Vector" d="M8.78581 12.8144L8.7858 12.8144L8.78075 12.8161C8.42892 12.9374 8.04691 13 7.63125 13C7.0617 13 6.56387 12.8877 6.12788 12.6731C5.69479 12.4578 5.33776 12.1536 5.05203 11.7548C4.85919 11.4783 4.71002 11.1643 4.60653 10.8088C4.60578 10.8063 4.60572 10.805 4.60571 10.8049L4.60571 10.8049L4.60573 10.8045C4.60577 10.8043 4.60639 10.801 4.61027 10.7953C4.61881 10.7828 4.63803 10.7668 4.66787 10.7613C4.69066 10.7571 4.71986 10.7599 4.75705 10.7832C4.79698 10.8084 4.83722 10.8531 4.86164 10.9117C5.05932 11.386 5.35201 11.7865 5.74021 12.1031L5.74019 12.1031L5.74442 12.1065C6.30071 12.5503 6.95605 12.7692 7.6875 12.7692C8.15265 12.7692 8.58829 12.6851 8.98574 12.507L8.98575 12.507L8.98917 12.5054C9.39374 12.3204 9.72685 12.0507 9.97355 11.6945C10.2302 11.3238 10.35 10.8938 10.35 10.4266C10.35 10.2805 10.3386 10.1376 10.313 10H10.4865C10.4944 10.0483 10.5 10.1354 10.5 10.3537C10.5 10.8007 10.4227 11.1786 10.2807 11.4978L10.2807 11.4978L10.2789 11.5019C10.1366 11.8294 9.94116 12.0969 9.69311 12.3122C9.437 12.5295 9.13644 12.6977 8.78581 12.8144Z" fill="#374151" stroke="currentColor"/> <path id="Vector" d="M2.66663 8H13.3333" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M7.07937 7.54091L6.69045 7.41689C6.69002 7.41675 6.68958 7.41661 6.68914 7.41648C6.4336 7.33643 6.17378 7.23601 5.9098 7.11611L5.89921 7.11129L5.89927 7.11117C5.59699 6.96529 5.33529 6.76737 5.12024 6.51703L5.11627 6.51241L5.1163 6.51238C4.85636 6.2022 4.74739 5.81798 4.74739 5.40605C4.74739 4.97764 4.87095 4.58359 5.12364 4.24341C5.36495 3.91369 5.68549 3.66379 6.06999 3.4901C6.45874 3.31449 6.88407 3.23467 7.33682 3.24033L7.07937 7.54091ZM7.07937 7.54091L6.14982 7.55395C5.94013 7.48004 5.73664 7.39061 5.53917 7.28553C5.3363 7.17295 5.15642 7.03547 4.99793 6.87257C4.85081 6.71669 4.72946 6.52733 4.63646 6.29832C4.55046 6.08189 4.5 5.80784 4.5 5.46475C4.5 4.91851 4.62632 4.48585 4.85438 4.14174C5.09772 3.77635 5.42558 3.49592 5.84993 3.29831C6.27823 3.09886 6.77169 2.99574 7.34021 3.00013L7.34026 3.00014C7.91665 3.00453 8.41327 3.11833 8.84083 3.32988L8.84082 3.32991L8.84588 3.33235C9.27908 3.54065 9.63452 3.83839 9.91773 4.23096L9.91772 4.23096L9.91958 4.23351C10.1115 4.49596 10.2638 4.79674 10.3746 5.13963C10.3771 5.14708 10.3767 5.15078 10.3764 5.15294C10.3759 5.15595 10.3743 5.1621 10.3688 5.17053C10.3569 5.18863 10.3317 5.20933 10.2942 5.21656C10.2642 5.22235 10.2252 5.21858 10.1748 5.18558C10.1209 5.15027 10.0671 5.08814 10.0343 5.00929C9.94734 4.80085 9.83757 4.60662 9.70464 4.42792C9.43271 4.05163 9.08781 3.75898 8.67415 3.55645C8.26193 3.34973 7.81366 3.2461 7.33709 3.24033L7.07937 7.54091Z" fill="#374151" stroke="currentColor"/> <path id="Vector 20" d="M3.85999 10.5179C4.61252 14.2805 12.1379 14.2805 12.138 10.5183C12.1381 9.01747 10.5701 8.46842 8.8656 8.00492M12.138 5.25053C11.3855 1.48786 4.61295 2.14734 4.6127 5.25053C4.61266 5.72038 4.76898 6.09592 5.03657 6.40442" stroke="currentColor" stroke-width="1.33" stroke-linecap="round"/>
<path id="Vector_3" d="M2.66675 8H13.3334" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 580 B

4
packages/nc-gui/components/account/Profile.vue

@ -32,7 +32,7 @@ const onSubmit = async () => {
isErrored.value = false isErrored.value = false
try { try {
await updateUserProfile({ attrs: { display_name: form.value.title } }) await updateUserProfile({ attrs: { display_name: form.value?.title } })
} catch (e: any) { } catch (e: any) {
console.error(e) console.error(e)
} finally { } finally {
@ -102,7 +102,7 @@ const onValidate = async (_: any, valid: boolean) => {
<NcButton <NcButton
type="primary" type="primary"
html-type="submit" html-type="submit"
:disabled="isErrored || (form.title && form.title === user?.display_name)" :disabled="isErrored || (form?.title && form?.title === user?.display_name)"
:loading="isTitleUpdating" :loading="isTitleUpdating"
data-testid="nc-account-settings-save" data-testid="nc-account-settings-save"
@click="onSubmit" @click="onSubmit"

14
packages/nc-gui/components/cell/RichText/LinkOptions.vue

@ -190,12 +190,12 @@ const tabIndex = computed(() => {
<div <div
v-if="!justDeleted" v-if="!justDeleted"
ref="wrapperRef" ref="wrapperRef"
class="relative bubble-menu nc-text-area-rich-link-options flex flex-col bg-gray-50 py-1 px-1 rounded-lg w-full" class="relative bubble-menu nc-text-area-rich-link-options bg-white flex flex-col border-1 border-gray-200 py-1 px-1 rounded-lg w-full"
data-testid="nc-text-area-rich-link-options" data-testid="nc-text-area-rich-link-options"
@keydown.stop="handleKeyDown" @keydown.stop="handleKeyDown"
> >
<div class="flex items-center gap-x-1"> <div class="flex items-center gap-x-1">
<div class="!border-1 !border-gray-200 !py-0.5 bg-gray-100 rounded-md !z-10 flex-1"> <div class="!py-0.5 bg-white rounded-md !z-10 flex-1">
<a-input <a-input
ref="inputRef" ref="inputRef"
v-model:value="href" v-model:value="href"
@ -216,12 +216,12 @@ const tabIndex = computed(() => {
:class="{ :class="{
'!text-gray-300 cursor-not-allowed': href.length === 0, '!text-gray-300 cursor-not-allowed': href.length === 0,
}" }"
data-testid="nc-text-area-rich-link-options-open-link" data-testid="text-gray-700 nc-text-area-rich-link-options-open-link"
size="small" size="small"
type="text" type="text"
@click="openLink" @click="openLink"
> >
<IcBaselineArrowOutward /> <GeneralIcon icon="externalLink" />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
<NcTooltip overlay-class-name="nc-text-area-rich-link-options"> <NcTooltip overlay-class-name="nc-text-area-rich-link-options">
@ -234,7 +234,7 @@ const tabIndex = computed(() => {
type="text" type="text"
@click="onDelete" @click="onDelete"
> >
<MdiDeleteOutline /> <GeneralIcon icon="delete" />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
</div> </div>
@ -248,6 +248,10 @@ const tabIndex = computed(() => {
@apply shadow-gray-200 shadow-sm; @apply shadow-gray-200 shadow-sm;
} }
.nc-text-area-rich-link-option-input {
@apply !placeholder:text-gray-500 text-gray-800;
}
.nc-text-area-rich-link-options { .nc-text-area-rich-link-options {
.ant-popover-inner-content { .ant-popover-inner-content {
@apply !shadow-none !p-0; @apply !shadow-none !p-0;

195
packages/nc-gui/components/smartsheet/expanded-form/Comments.vue

@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { CommentType } from 'nocodb-sdk' import { type CommentType, ProjectRoles } from 'nocodb-sdk'
const props = defineProps<{ const props = defineProps<{
loading: boolean loading: boolean
primaryKey: string
}>() }>()
const { const {
@ -32,6 +33,14 @@ const { dashboardUrl } = useDashboard()
const { user, appInfo } = useGlobal() const { user, appInfo } = useGlobal()
const basesStore = useBases()
const { basesUser } = storeToRefs(basesStore)
const meta = inject(MetaInj, ref())
const baseUsers = computed(() => (meta.value?.base_id ? basesUser.value.get(meta.value?.base_id) || [] : []))
const isExpandedFormLoading = computed(() => props.loading) const isExpandedFormLoading = computed(() => props.loading)
const tab = ref<'comments' | 'audits'>('comments') const tab = ref<'comments' | 'audits'>('comments')
@ -42,34 +51,22 @@ const router = useRouter()
const hasEditPermission = computed(() => isUIAllowed('commentEdit')) const hasEditPermission = computed(() => isUIAllowed('commentEdit'))
const editComment = ref<CommentType>() const editCommentValue = ref<CommentType>()
const isEditing = ref<boolean>(false) const isEditing = ref<boolean>(false)
const isCommentMode = ref(false) const isCommentMode = ref(false)
function onKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
onKeyEsc(event)
} else if (event.key === 'Enter' && !event.shiftKey) {
onKeyEnter(event)
}
}
function onKeyEnter(event: KeyboardEvent) {
event.stopImmediatePropagation()
event.preventDefault()
onEditComment()
}
function onKeyEsc(event: KeyboardEvent) {
event.stopImmediatePropagation()
event.preventDefault()
onCancel()
}
async function onEditComment() { async function onEditComment() {
if (!isEditing.value || !editComment.value) return if (!isEditing.value || !editCommentValue.value?.comment) return
while (editCommentValue.value.comment.endsWith('<br />') || editCommentValue.value.comment.endsWith('\n')) {
if (editCommentValue.value.comment.endsWith('<br />')) {
editCommentValue.value.comment = editCommentValue.value.comment.slice(0, -6)
} else {
editCommentValue.value.comment = editCommentValue.value.comment.slice(0, -2)
}
}
isCommentMode.value = true isCommentMode.value = true
@ -85,26 +82,20 @@ async function onEditComment() {
loadComments() loadComments()
} }
function onCancel() { function onCancel(e: KeyboardEvent) {
if (!isEditing.value) return if (!isEditing.value) return
editComment.value = undefined e.preventDefault()
onStopEdit() e.stopPropagation()
} editCommentValue.value = undefined
function onStopEdit() {
loadComments() loadComments()
isEditing.value = false isEditing.value = false
editComment.value = undefined editCommentValue.value = undefined
} }
onKeyStroke('Enter', (event) => { function editComment(comment: CommentType) {
if (isEditing.value) { editCommentValue.value = {
onKeyEnter(event) ...comment,
} }
})
function editComments(comment: CommentType) {
editComment.value = comment
isEditing.value = true isEditing.value = true
nextTick(() => { nextTick(() => {
scrollToComment(comment.id) scrollToComment(comment.id)
@ -113,11 +104,11 @@ function editComments(comment: CommentType) {
const value = computed({ const value = computed({
get() { get() {
return editComment.value?.comment || '' return editCommentValue.value?.comment || ''
}, },
set(val) { set(val) {
if (!editComment.value) return if (!editCommentValue.value) return
editComment.value.comment = val editCommentValue.value.comment = val
}, },
}) })
@ -142,9 +133,8 @@ const saveComment = async () => {
} }
isCommentMode.value = true isCommentMode.value = true
isSaving.value = true
// Optimistic Insert
// Optimistic Insert
comments.value = [ comments.value = [
...comments.value, ...comments.value,
{ {
@ -173,11 +163,17 @@ const saveComment = async () => {
scrollComments() scrollComments()
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} finally {
isSaving.value = false
} }
} }
const copyComment = async (comment: CommentType) => {
await copy(
encodeURI(
`${dashboardUrl?.value}#/${route.params.typeOrId}/${route.params.baseId}/${meta.value?.id}?rowId=${props.primaryKey}&commentId=${comment.id}`,
),
)
}
function scrollToComment(commentId: string) { function scrollToComment(commentId: string) {
const commentEl = document.querySelector(`.${commentId}`) const commentEl = document.querySelector(`.${commentId}`)
if (commentEl) { if (commentEl) {
@ -207,10 +203,6 @@ watch(commentsWrapperEl, () => {
}, 100) }, 100)
}) })
const timesAgo = (comment: CommentType) => {
return comment.created_at !== comment.updated_at ? `Edited ${timeAgo(comment.updated_at!)}` : timeAgo(comment.created_at!)
}
const createdBy = ( const createdBy = (
comment: CommentType & { comment: CommentType & {
created_display_name?: string created_display_name?: string
@ -219,7 +211,7 @@ const createdBy = (
if (comment.created_by === user.value?.id) { if (comment.created_by === user.value?.id) {
return 'You' return 'You'
} else if (comment.created_display_name?.trim()) { } else if (comment.created_display_name?.trim()) {
return comment.created_by_email || 'Shared source' return comment.created_display_name || 'Shared source'
} else if (comment.created_by_email) { } else if (comment.created_by_email) {
return comment.created_by_email return comment.created_by_email
} else { } else {
@ -244,7 +236,7 @@ const editedAt = (comment: CommentType) => {
</script> </script>
<template> <template>
<div class="flex flex-col !h-full w-full"> <div class="flex flex-col bg-white !h-full w-full rounded-br-2xl">
<NcTabs v-model:activeKey="tab" class="h-full"> <NcTabs v-model:activeKey="tab" class="h-full">
<a-tab-pane key="comments" class="w-full h-full"> <a-tab-pane key="comments" class="w-full h-full">
<template #tab> <template #tab>
@ -259,8 +251,8 @@ const editedAt = (comment: CommentType) => {
'pb-1': tab !== 'comments' && !appInfo.ee, 'pb-1': tab !== 'comments' && !appInfo.ee,
}" }"
> >
<div v-if="isExpandedFormLoading" class="flex flex-col h-full"> <div v-if="isExpandedFormLoading" class="flex flex-col items-center justify-center w-full h-full">
<GeneralLoader class="!mt-16" size="xlarge" /> <GeneralLoader size="xlarge" />
</div> </div>
<div v-else class="flex flex-col h-full"> <div v-else class="flex flex-col h-full">
<div v-if="comments.length === 0" class="flex flex-col my-1 text-center justify-center h-full nc-scrollbar-thin"> <div v-if="comments.length === 0" class="flex flex-col my-1 text-center justify-center h-full nc-scrollbar-thin">
@ -270,6 +262,11 @@ const editedAt = (comment: CommentType) => {
<div class="font-medium text-center my-6 text-gray-500">{{ $t('activity.startCommenting') }}</div> <div class="font-medium text-center my-6 text-gray-500">{{ $t('activity.startCommenting') }}</div>
</div> </div>
<div v-else ref="commentsWrapperEl" class="flex flex-col h-full py-1 nc-scrollbar-thin"> <div v-else ref="commentsWrapperEl" class="flex flex-col h-full py-1 nc-scrollbar-thin">
<!-- The scrollbar doesn't work when flex-end is used. https://issues.chromium.org/issues/41130651
Hence using a div to fix the issue
https://stackoverflow.com/questions/36130760/use-justify-content-flex-end-and-to-have-vertical-scrollbar
-->
<div class="scroll-fix"></div>
<div v-for="comment of comments" :key="comment.id" :class="`${comment.id}`" class="nc-comment-item"> <div v-for="comment of comments" :key="comment.id" :class="`${comment.id}`" class="nc-comment-item">
<div <div
:class="{ :class="{
@ -286,28 +283,54 @@ const editedAt = (comment: CommentType) => {
size="medium" size="medium"
/> />
<div class="flex h-[28px] items-center gap-3"> <div class="flex h-[28px] items-center gap-3">
<NcTooltip class="truncate capitalize text-gray-800 font-weight-700 !text-[13px] max-w-42"> <NcDropdown placement="topLeft" :trigger="['hover']">
<template #title> <span class="text-ellipsis text-gray-800 font-medium !text-[13px] max-w-42 overflow-hidden" :style="{}">
{{ comment.created_display_name?.trim() || comment.created_by_email || 'Shared source' }}
</template>
<span class="text-ellipsis capitalize overflow-hidden" :style="{}">
{{ createdBy(comment) }} {{ createdBy(comment) }}
</span> </span>
</NcTooltip>
<template #overlay>
<div class="bg-white rounded-lg">
<div class="flex items-center gap-4 py-3 px-2">
<GeneralUserIcon
class="!w-8 !h-8 border-1 border-gray-200 rounded-full"
:name="comment.created_display_name"
:email="comment.created_by_email"
/>
<div class="flex flex-col">
<div class="font-semibold text-gray-800">
{{ createdBy(comment) }}
</div>
<div class="text-xs text-gray-600">
{{ comment.created_by_email }}
</div>
</div>
</div>
<div
class="px-3 rounded-b-lg !text-[13px] items-center text-gray-600 flex gap-1 bg-gray-100 py-1.5"
>
Has <RolesBadge size="sm" :border="false" :role="getUserRole(comment.created_by_email!)" />
role in base
</div>
</div>
</template>
</NcDropdown>
<div class="text-xs text-gray-500"> <div class="text-xs text-gray-500">
{{ timesAgo(comment) }} {{ timeAgo(comment.created_at!) }}
</div> </div>
</div> </div>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center">
<NcDropdown <NcDropdown
v-if="!editCommentValue" v-if="!editCommentValue"
class="!hidden !group-hover:block" class="!hidden !group-hover:block"
overlay-class-name="!min-w-[160px]" overlay-class-name="!min-w-[160px]"
placement="bottomRight" placement="bottomRight"
> >
<NcButton class="nc-expand-form-more-actions !w-7 !h-7 !bg-transparent" size="xsmall" type="text"> <NcButton
class="nc-expand-form-more-actions !hover:bg-gray-200 !w-7 !h-7 !bg-transparent"
size="xsmall"
type="text"
>
<GeneralIcon class="text-md" icon="threeDotVertical" /> <GeneralIcon class="text-md" icon="threeDotVertical" />
</NcButton> </NcButton>
<template #overlay> <template #overlay>
@ -316,7 +339,7 @@ const editedAt = (comment: CommentType) => {
v-if="user && comment.created_by_email === user.email" v-if="user && comment.created_by_email === user.email"
v-e="['c:comment-expand:comment:edit']" v-e="['c:comment-expand:comment:edit']"
class="text-gray-700" class="text-gray-700"
@click="editComments(comment)" @click="editComment(comment)"
> >
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<component :is="iconMap.rename" class="cursor-pointer" /> <component :is="iconMap.rename" class="cursor-pointer" />
@ -349,11 +372,10 @@ const editedAt = (comment: CommentType) => {
</NcMenu> </NcMenu>
</template> </template>
</NcDropdown> </NcDropdown>
<div v-if="appInfo.ee"> <div v-if="appInfo.ee">
<NcTooltip v-if="!comment.resolved_by"> <NcTooltip v-if="!comment.resolved_by">
<NcButton <NcButton
class="!w-7 !h-7 !bg-transparent opacity-0 group-hover:opacity-100" class="!w-7 !h-7 !bg-transparent !hover:bg-gray-200 !hidden !group-hover:block"
size="xsmall" size="xsmall"
type="text" type="text"
@click="resolveComment(comment.id!)" @click="resolveComment(comment.id!)"
@ -366,19 +388,24 @@ const editedAt = (comment: CommentType) => {
<NcTooltip v-else> <NcTooltip v-else>
<template #title>{{ `Resolved by ${comment.resolved_display_name}` }}</template> <template #title>{{ `Resolved by ${comment.resolved_display_name}` }}</template>
<div class="flex text-[#17803D] font-semibold items-center"> <NcButton
<NcButton class="!h-7 !bg-transparent" size="xsmall" type="text" @click="resolveComment(comment.id)"> class="!h-7 !w-7 !bg-transparent !hover:bg-gray-200 text-semibold"
<div class="flex items-center gap-2 !text-[#17803D]"> size="xsmall"
<span> Resolved </span> type="text"
<component :is="iconMap.checkCircle" /> @click="resolveComment(comment.id)"
</div> >
<GeneralIcon class="text-md rounded-full bg-[#17803D] text-white" icon="checkFill" />
</NcButton> </NcButton>
</div>
</NcTooltip> </NcTooltip>
</div> </div>
</div> </div>
</div> </div>
<div class="flex-1 flex flex-col gap-1 mt-1 max-w-[calc(100%)]"> <div
:class="{
'mt-3': comment.id === editCommentValue?.id,
}"
class="flex-1 flex flex-col gap-1 max-w-[calc(100%)]"
>
<SmartsheetExpandedFormRichComment <SmartsheetExpandedFormRichComment
v-if="comment.id === editCommentValue?.id" v-if="comment.id === editCommentValue?.id"
v-model:value="value" v-model:value="value"
@ -388,19 +415,19 @@ const editedAt = (comment: CommentType) => {
data-testid="expanded-form-comment-input" data-testid="expanded-form-comment-input"
sync-value-change sync-value-change
@save="onEditComment" @save="onEditComment"
@keydown.stop="onKeyDown" @keydown.esc="onCancel"
@blur=" @blur="
() => { () => {
editComment = undefined editCommentValue = undefined
isEditing = false isEditing = false
} }
" "
@keydown.enter.exact.prevent="onEditComment" @keydown.enter.exact.prevent="onEditComment"
/> />
<div v-else class="text-small pl-9 leading-18px text-gray-800"> <div v-else class="space-y-1 pl-9">
<SmartsheetExpandedFormRichComment <SmartsheetExpandedFormRichComment
:value="comment.comment" :value="`${comment.comment} ${editedAt(comment)}`"
class="!text-small !leading-18px !text-gray-800 -ml-1" class="!text-small !leading-18px !text-gray-800 -ml-1"
read-only read-only
sync-value-change sync-value-change
@ -410,7 +437,7 @@ const editedAt = (comment: CommentType) => {
</div> </div>
</div> </div>
</div> </div>
<div v-if="hasEditPermission" class="bg-gray-50 nc-comment-input !rounded-br-2xl gap-2 flex"> <div v-if="hasEditPermission" class="px-3 pb-3 nc-comment-input !rounded-br-2xl gap-2 flex">
<SmartsheetExpandedFormRichComment <SmartsheetExpandedFormRichComment
ref="commentInputRef" ref="commentInputRef"
v-model:value="comment" v-model:value="comment"
@ -451,8 +478,8 @@ const editedAt = (comment: CommentType) => {
'pb-1': !appInfo.ee, 'pb-1': !appInfo.ee,
}" }"
> >
<div v-if="isExpandedFormLoading || isAuditLoading" class="flex flex-col h-full"> <div v-if="isExpandedFormLoading || isAuditLoading" class="flex flex-col items-center justify-center w-full h-full">
<GeneralLoader class="!mt-16" size="xlarge" /> <GeneralLoader size="xlarge" />
</div> </div>
<div v-else ref="commentsWrapperEl" class="flex flex-col h-full py-1 nc-scrollbar-thin !overflow-y-auto"> <div v-else ref="commentsWrapperEl" class="flex flex-col h-full py-1 nc-scrollbar-thin !overflow-y-auto">
@ -498,10 +525,8 @@ const editedAt = (comment: CommentType) => {
@apply max-w-1/2; @apply max-w-1/2;
} }
.nc-comment-input { .scroll-fix {
:deep(.nc-comment-rich-editor) { flex: 1 1 auto;
@apply !ml-1;
}
} }
.nc-audit-item { .nc-audit-item {
@ -534,7 +559,7 @@ const editedAt = (comment: CommentType) => {
:deep(.ant-tabs) { :deep(.ant-tabs) {
@apply !overflow-visible; @apply !overflow-visible;
.ant-tabs-nav { .ant-tabs-nav {
@apply px-3; @apply px-3 bg-white;
.ant-tabs-nav-list { .ant-tabs-nav-list {
@apply w-[99%] mx-auto gap-6; @apply w-[99%] mx-auto gap-6;
@ -568,6 +593,6 @@ const editedAt = (comment: CommentType) => {
} }
:deep(.expanded-form-comment-edit-input .nc-comment-rich-editor) { :deep(.expanded-form-comment-edit-input .nc-comment-rich-editor) {
@apply !pl-2 bg-white; @apply bg-white;
} }
</style> </style>

89
packages/nc-gui/components/smartsheet/expanded-form/RichComment.vue

@ -6,6 +6,7 @@ import { marked } from 'marked'
import { generateJSON } from '@tiptap/html' import { generateJSON } from '@tiptap/html'
import Underline from '@tiptap/extension-underline' import Underline from '@tiptap/extension-underline'
import Placeholder from '@tiptap/extension-placeholder' import Placeholder from '@tiptap/extension-placeholder'
import tippy from 'tippy.js'
import { Link } from '~/helpers/dbTiptapExtensions/links' import { Link } from '~/helpers/dbTiptapExtensions/links'
const props = withDefaults( const props = withDefaults(
@ -74,10 +75,16 @@ const tiptapExtensions = [
const editor = useEditor({ const editor = useEditor({
extensions: tiptapExtensions, extensions: tiptapExtensions,
onUpdate: ({ editor }) => { onUpdate: ({ editor }) => {
const markdown = turndownService let markdown = turndownService.turndown(editor.getHTML().replaceAll(/<p><\/p>/g, '<br />'))
.turndown(editor.getHTML().replaceAll(/<p><\/p>/g, '<br />'))
.replaceAll(/\n\n<br \/>\n\n/g, '<br>\n\n') const isListsActive = editor?.isActive('bulletList') || editor?.isActive('orderedList') || editor?.isActive('blockquote')
vModel.value = markdown === '<br />' ? '' : markdown if (isListsActive) {
if (markdown.endsWith('<br />')) markdown = markdown.slice(0, -6)
}
markdown = markdown.replaceAll(/\n\n<br \/>\n\n/g, '<br>\n\n')
vModel.value = markdown === '<br />' ? '' : `${markdown}`
}, },
editable: !props.readOnly, editable: !props.readOnly,
autofocus: props.autofocus, autofocus: props.autofocus,
@ -87,8 +94,12 @@ const editor = useEditor({
onFocusWrapper() onFocusWrapper()
}, },
onBlur: (e) => { onBlur: (e) => {
const targetEl = e?.event.relatedTarget as HTMLElement
if ( if (
!(e?.event?.relatedTarget as HTMLElement)?.closest('.comment-bubble-menu, .nc-comment-rich-editor, .nc-rich-text-comment') !targetEl.closest(
'.comment-bubble-menu, .nc-rich-text-comment, .tippy-box, .nc-comment-save-btn, .rich-text-bottom-bar, .mention, .nc-mention-list, .tippy-content, .nc-comment-rich-editor',
)
) { ) {
isFocused.value = false isFocused.value = false
emits('blur') emits('blur')
@ -142,7 +153,9 @@ useEventListener(
const targetEl = e?.relatedTarget as HTMLElement const targetEl = e?.relatedTarget as HTMLElement
if ( if (
targetEl?.classList?.contains('tiptap') || targetEl?.classList?.contains('tiptap') ||
!targetEl?.closest('.comment-bubble-menu, .tippy-content, .nc-comment-rich-editor') !targetEl?.closest(
'.comment-bubble-menu, .nc-rich-text-comment, .tippy-box, .nc-comment-save-btn, .rich-text-bottom-bar, .mention, .nc-mention-list, .tippy-content, .nc-comment-rich-editor',
)
) { ) {
isFocused.value = false isFocused.value = false
emits('blur') emits('blur')
@ -155,10 +168,15 @@ useEventListener(
'focusout', 'focusout',
(e: FocusEvent) => { (e: FocusEvent) => {
const targetEl = e?.relatedTarget as HTMLElement const targetEl = e?.relatedTarget as HTMLElement
if (!targetEl && (e.target as HTMLElement)?.closest('.comment-bubble-menu, .tippy-content, .nc-comment-rich-editor')) return if (
!targetEl &&
(e.target as HTMLElement)?.closest('.comment-bubble-menu, .nc-comment-save-btn, .tippy-content, .nc-comment-rich-editor')
)
return
if (!targetEl?.closest('.comment-bubble-menu, .tippy-content, .nc-comment-rich-editor')) { if (!targetEl?.closest('.comment-bubble-menu, .nc-comment-save-btn, .tippy-content, .nc-comment-rich-editor')) {
isFocused.value = false isFocused.value = false
emits('blur') emits('blur')
} }
}, },
@ -169,7 +187,11 @@ onClickOutside(editorDom, (e) => {
const targetEl = e?.target as HTMLElement const targetEl = e?.target as HTMLElement
if (!targetEl?.closest('.tippy-content, .comment-bubble-menu, .nc-comment-rich-editor')) { if (
!targetEl?.closest(
'.tippy-content, .nc-rich-text-comment, .nc-comment-save-btn, .comment-bubble-menu, .nc-comment-rich-editor',
)
) {
isFocused.value = false isFocused.value = false
emits('blur') emits('blur')
} }
@ -183,7 +205,7 @@ const emitSave = (event: KeyboardEvent) => {
// If Enter was pressed in the list, do not emit save // If Enter was pressed in the list, do not emit save
triggerSaveFromList.value = false triggerSaveFromList.value = false
} else { } else {
if (editor.value.isActive('bulletList') || editor.value.isActive('orderedList')) { if (editor.value.isActive('bulletList') || editor.value.isActive('orderedList') || editor.value.isActive('blockquote')) {
event.stopPropagation() event.stopPropagation()
} else { } else {
emits('save') emits('save')
@ -193,7 +215,8 @@ const emitSave = (event: KeyboardEvent) => {
} }
const handleEnterDown = (event: KeyboardEvent) => { const handleEnterDown = (event: KeyboardEvent) => {
const isListsActive = editor.value?.isActive('bulletList') || editor.value?.isActive('orderedList') const isListsActive =
editor.value?.isActive('bulletList') || editor.value?.isActive('orderedList') || editor.value.isActive('blockquote')
if (isListsActive) { if (isListsActive) {
triggerSaveFromList.value = true triggerSaveFromList.value = true
setTimeout(() => { setTimeout(() => {
@ -220,6 +243,31 @@ const handleKeyPress = (event: KeyboardEvent) => {
defineExpose({ defineExpose({
setEditorContent, setEditorContent,
}) })
onMounted(() => {
if (!props.readOnly) return
setTimeout(() => {
document.querySelectorAll('.nc-rich-link-tooltip').forEach((el) => {
const tooltip = Object.values(el.attributes).find((attr) => attr.name === 'data-tooltip')
if (!tooltip) return
tippy(el, {
content: `<span class="tooltip">${tooltip.value}</span>`,
placement: 'top',
allowHTML: true,
arrow: true,
animation: 'fade',
duration: 0,
})
})
}, 1000)
})
const saveComment = (e) => {
e.preventDefault()
e.stopPropagation()
emits('save')
}
</script> </script>
<template> <template>
@ -249,21 +297,21 @@ defineExpose({
ref="editorDom" ref="editorDom"
:editor="editor" :editor="editor"
:class="{ :class="{
'px-1.5': !props.readOnly, 'px-2': !props.readOnly,
'px-[0.25rem]': props.readOnly, 'px-[0.25rem]': props.readOnly,
}" }"
class="flex flex-col nc-comment-rich-editor w-full scrollbar-thin scrollbar-thumb-gray-200 nc-truncate scrollbar-track-transparent" class="flex flex-col nc-comment-rich-editor py-2.125 w-full scrollbar-thin scrollbar-thumb-gray-200 nc-truncate scrollbar-track-transparent"
@keydown.stop="handleKeyPress" @keydown.stop="handleKeyPress"
/> />
<div v-if="!hideOptions" class="flex justify-between px-2 py-2 items-center"> <div v-if="!hideOptions" class="flex justify-between p-2 items-center">
<LazySmartsheetExpandedFormRichTextOptions :editor="editor" class="!bg-transparent" /> <LazySmartsheetExpandedFormRichTextOptions :editor="editor" class="!bg-transparent" />
<NcButton <NcButton
v-e="['a:row-expand:comment:save']" v-e="['a:row-expand:comment:save']"
:disabled="!vModel?.length" :disabled="!vModel?.length"
class="!disabled:bg-gray-100 !h-7 !w-7 !shadow-none" class="!disabled:bg-gray-100 nc-comment-save-btn !h-7 !w-7 !shadow-none"
size="xsmall" size="xsmall"
@click="emits('save')" @click="saveComment"
> >
<GeneralIcon icon="send" /> <GeneralIcon icon="send" />
</NcButton> </NcButton>
@ -273,6 +321,10 @@ defineExpose({
</template> </template>
<style lang="scss"> <style lang="scss">
.tooltip {
@apply text-xs bg-gray-800 text-white px-2 py-1 rounded-lg;
}
.nc-rich-text-comment { .nc-rich-text-comment {
.readonly { .readonly {
.nc-comment-rich-editor { .nc-comment-rich-editor {
@ -282,6 +334,11 @@ defineExpose({
} }
} }
} }
.nc-rich-link-tooltip {
@apply text-gray-500;
}
.nc-comment-rich-editor { .nc-comment-rich-editor {
&.nc-truncate { &.nc-truncate {
.tiptap.ProseMirror { .tiptap.ProseMirror {

2
packages/nc-gui/components/smartsheet/expanded-form/index.vue

@ -950,7 +950,7 @@ export default {
:class="{ active: commentsDrawer && isUIAllowed('commentList') }" :class="{ active: commentsDrawer && isUIAllowed('commentList') }"
class="nc-comments-drawer border-l-1 relative border-gray-200 bg-gray-50 w-1/3 max-w-[340px] min-w-0 h-full xs:hidden rounded-br-2xl" class="nc-comments-drawer border-l-1 relative border-gray-200 bg-gray-50 w-1/3 max-w-[340px] min-w-0 h-full xs:hidden rounded-br-2xl"
> >
<SmartsheetExpandedFormComments :loading="isLoading" /> <SmartsheetExpandedFormComments :primary-key="primaryKey" :loading="isLoading" />
</div> </div>
</div> </div>
</div> </div>

11
packages/nc-gui/helpers/dbTiptapExtensions/links.ts

@ -19,6 +19,7 @@ export const Link = TiptapLink.extend({
internal: false, internal: false,
} }
}, },
addAttributes() { addAttributes() {
return { return {
href: { href: {
@ -36,7 +37,17 @@ export const Link = TiptapLink.extend({
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
const attr = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes) const attr = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)
// We use this as a workaround to show a tooltip on the content
// We use the href to store the tooltip content
if (isValidURL(attr.href) || !attr.href.includes('~~~###~~~')) {
return ['a', attr, 0] return ['a', attr, 0]
}
// The class is used to identify the text that needs to show the tooltip
// The data-tooltip is the content of the tooltip
attr.class = 'nc-rich-link-tooltip'
attr['data-tooltip'] = attr.href?.split('~~~###~~~')[1]?.replace(/_/g, ' ')
return ['span', attr]
}, },
addKeyboardShortcuts() { addKeyboardShortcuts() {

4
packages/nc-gui/utils/iconUtils.ts

@ -132,6 +132,8 @@ import NcArrowUpRight from '~icons/nc-icons/arrow-up-right'
import NcSlash from '~icons/nc-icons/slash' import NcSlash from '~icons/nc-icons/slash'
import NcNotification from '~icons/nc-icons/bell' import NcNotification from '~icons/nc-icons/bell'
import NcCheckCircle from '~icons/nc-icons/check-circle' import NcCheckCircle from '~icons/nc-icons/check-circle'
import NcCheckFill from '~icons/nc-icons/checkFill'
import NcExternalLink from '~icons/nc-icons/external-link'
// import NcProjectGray from '~icons/nc-icons/project-gray' // import NcProjectGray from '~icons/nc-icons/project-gray'
import NcPhoneCall from '~icons/nc-icons/phone-call' import NcPhoneCall from '~icons/nc-icons/phone-call'
@ -405,6 +407,8 @@ export const iconMap = {
search: NcSearch, search: NcSearch,
calendar: Calendar, calendar: Calendar,
checkCircle: NcCheckCircle, checkCircle: NcCheckCircle,
checkFill: NcCheckFill,
externalLink: NcExternalLink,
error: h('span', { class: 'material-symbols' }, 'error'), error: h('span', { class: 'material-symbols' }, 'error'),
info: h(MsInfo, {}, () => 'info'), info: h(MsInfo, {}, () => 'info'),
inbox: h('span', { class: 'material-symbols' }, 'inbox'), inbox: h('span', { class: 'material-symbols' }, 'inbox'),

7
packages/nocodb/src/meta/meta.service.ts

@ -587,6 +587,7 @@ export class MetaService {
* @param data - Data to be updated * @param data - Data to be updated
* @param idOrCondition - If string, will update the record with the given id. If object, will update the record with the given condition. * @param idOrCondition - If string, will update the record with the given id. If object, will update the record with the given condition.
* @param xcCondition - Additional nested or complex condition to be added to the query. * @param xcCondition - Additional nested or complex condition to be added to the query.
* @param skipUpdatedAt - If true, will not update the updated_at field
* @param force - If true, will not check if a condition is present in the query builder and will execute the query as is. * @param force - If true, will not check if a condition is present in the query builder and will execute the query as is.
*/ */
public async metaUpdate( public async metaUpdate(
@ -596,6 +597,7 @@ export class MetaService {
data: any, data: any,
idOrCondition?: string | { [p: string]: any }, idOrCondition?: string | { [p: string]: any },
xcCondition?: Condition, xcCondition?: Condition,
skipUpdatedAt = false,
force = false, force = false,
): Promise<any> { ): Promise<any> {
const query = this.knexConnection(target); const query = this.knexConnection(target);
@ -625,7 +627,10 @@ export class MetaService {
delete data.created_at; delete data.created_at;
query.update({ ...data, updated_at: this.now() }); if (!skipUpdatedAt) {
data.updated_at = this.now();
}
query.update({ ...data });
if (typeof idOrCondition !== 'object') { if (typeof idOrCondition !== 'object') {
query.where('id', idOrCondition); query.where('id', idOrCondition);
} else if (idOrCondition) { } else if (idOrCondition) {

24
packages/nocodb/src/models/Comment.ts

@ -126,6 +126,30 @@ export default class Comment implements CommentType {
return Comment.get(context, commentId, ncMeta); return Comment.get(context, commentId, ncMeta);
} }
public static async resolve(
context,
commentId: string,
comment: Partial<Comment>,
ncMeta = Noco.ncMeta,
) {
const updateObj = extractProps(comment, [
'resolved_by',
'resolved_by_email',
]);
await ncMeta.metaUpdate(
context.workspace_id,
context.base_id,
MetaTable.COMMENTS,
prepareForDb(updateObj),
commentId,
{},
true,
);
return Comment.get(context, commentId, ncMeta);
}
static async delete( static async delete(
context: NcContext, context: NcContext,
commentId: string, commentId: string,

2
packages/nocodb/src/services/notifications/notifications.service.ts

@ -17,7 +17,7 @@ import { getCircularReplacer } from '~/utils';
import { PubSubRedis } from '~/redis/pubsub-redis'; import { PubSubRedis } from '~/redis/pubsub-redis';
@Injectable() @Injectable()
export class NotificationsService implements OnModuleInit, OnModuleDestroy { export class NotificationsService implements OnModuleInit, OnModuleDestroy {
private logger: Logger = new Logger(NotificationsService.name); protected logger: Logger = new Logger(NotificationsService.name);
constructor(protected readonly appHooks: AppHooksService) {} constructor(protected readonly appHooks: AppHooksService) {}

Loading…
Cancel
Save