Browse Source

Merge pull request #8728 from nocodb/develop

pull/8729/head 0.250.1
github-actions[bot] 2 weeks ago committed by GitHub
parent
commit
fc175edf95
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. 197
      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. 13
      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">
<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_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_3" d="M2.66675 8H13.3334" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector" d="M2.66663 8H13.3333" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<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"/>
</g>
</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
try {
await updateUserProfile({ attrs: { display_name: form.value.title } })
await updateUserProfile({ attrs: { display_name: form.value?.title } })
} catch (e: any) {
console.error(e)
} finally {
@ -102,7 +102,7 @@ const onValidate = async (_: any, valid: boolean) => {
<NcButton
type="primary"
html-type="submit"
:disabled="isErrored || (form.title && form.title === user?.display_name)"
:disabled="isErrored || (form?.title && form?.title === user?.display_name)"
:loading="isTitleUpdating"
data-testid="nc-account-settings-save"
@click="onSubmit"

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

@ -190,12 +190,12 @@ const tabIndex = computed(() => {
<div
v-if="!justDeleted"
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"
@keydown.stop="handleKeyDown"
>
<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
ref="inputRef"
v-model:value="href"
@ -216,12 +216,12 @@ const tabIndex = computed(() => {
:class="{
'!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"
type="text"
@click="openLink"
>
<IcBaselineArrowOutward />
<GeneralIcon icon="externalLink" />
</NcButton>
</NcTooltip>
<NcTooltip overlay-class-name="nc-text-area-rich-link-options">
@ -234,7 +234,7 @@ const tabIndex = computed(() => {
type="text"
@click="onDelete"
>
<MdiDeleteOutline />
<GeneralIcon icon="delete" />
</NcButton>
</NcTooltip>
</div>
@ -248,6 +248,10 @@ const tabIndex = computed(() => {
@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 {
.ant-popover-inner-content {
@apply !shadow-none !p-0;

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

@ -1,8 +1,9 @@
<script setup lang="ts">
import type { CommentType } from 'nocodb-sdk'
import { type CommentType, ProjectRoles } from 'nocodb-sdk'
const props = defineProps<{
loading: boolean
primaryKey: string
}>()
const {
@ -32,6 +33,14 @@ const { dashboardUrl } = useDashboard()
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 tab = ref<'comments' | 'audits'>('comments')
@ -42,34 +51,22 @@ const router = useRouter()
const hasEditPermission = computed(() => isUIAllowed('commentEdit'))
const editComment = ref<CommentType>()
const editCommentValue = ref<CommentType>()
const isEditing = ref<boolean>(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() {
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
@ -85,26 +82,20 @@ async function onEditComment() {
loadComments()
}
function onCancel() {
function onCancel(e: KeyboardEvent) {
if (!isEditing.value) return
editComment.value = undefined
onStopEdit()
}
function onStopEdit() {
e.preventDefault()
e.stopPropagation()
editCommentValue.value = undefined
loadComments()
isEditing.value = false
editComment.value = undefined
editCommentValue.value = undefined
}
onKeyStroke('Enter', (event) => {
if (isEditing.value) {
onKeyEnter(event)
function editComment(comment: CommentType) {
editCommentValue.value = {
...comment,
}
})
function editComments(comment: CommentType) {
editComment.value = comment
isEditing.value = true
nextTick(() => {
scrollToComment(comment.id)
@ -113,11 +104,11 @@ function editComments(comment: CommentType) {
const value = computed({
get() {
return editComment.value?.comment || ''
return editCommentValue.value?.comment || ''
},
set(val) {
if (!editComment.value) return
editComment.value.comment = val
if (!editCommentValue.value) return
editCommentValue.value.comment = val
},
})
@ -142,9 +133,8 @@ const saveComment = async () => {
}
isCommentMode.value = true
isSaving.value = true
// Optimistic Insert
// Optimistic Insert
comments.value = [
...comments.value,
{
@ -173,11 +163,17 @@ const saveComment = async () => {
scrollComments()
} catch (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) {
const commentEl = document.querySelector(`.${commentId}`)
if (commentEl) {
@ -207,10 +203,6 @@ watch(commentsWrapperEl, () => {
}, 100)
})
const timesAgo = (comment: CommentType) => {
return comment.created_at !== comment.updated_at ? `Edited ${timeAgo(comment.updated_at!)}` : timeAgo(comment.created_at!)
}
const createdBy = (
comment: CommentType & {
created_display_name?: string
@ -219,7 +211,7 @@ const createdBy = (
if (comment.created_by === user.value?.id) {
return 'You'
} 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) {
return comment.created_by_email
} else {
@ -244,7 +236,7 @@ const editedAt = (comment: CommentType) => {
</script>
<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">
<a-tab-pane key="comments" class="w-full h-full">
<template #tab>
@ -259,8 +251,8 @@ const editedAt = (comment: CommentType) => {
'pb-1': tab !== 'comments' && !appInfo.ee,
}"
>
<div v-if="isExpandedFormLoading" class="flex flex-col h-full">
<GeneralLoader class="!mt-16" size="xlarge" />
<div v-if="isExpandedFormLoading" class="flex flex-col items-center justify-center w-full h-full">
<GeneralLoader size="xlarge" />
</div>
<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">
@ -270,6 +262,11 @@ const editedAt = (comment: CommentType) => {
<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-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
:class="{
@ -286,28 +283,54 @@ const editedAt = (comment: CommentType) => {
size="medium"
/>
<div class="flex h-[28px] items-center gap-3">
<NcTooltip class="truncate capitalize text-gray-800 font-weight-700 !text-[13px] max-w-42">
<template #title>
{{ comment.created_display_name?.trim() || comment.created_by_email || 'Shared source' }}
</template>
<span class="text-ellipsis capitalize overflow-hidden" :style="{}">
<NcDropdown placement="topLeft" :trigger="['hover']">
<span class="text-ellipsis text-gray-800 font-medium !text-[13px] max-w-42 overflow-hidden" :style="{}">
{{ createdBy(comment) }}
</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">
{{ timesAgo(comment) }}
{{ timeAgo(comment.created_at!) }}
</div>
</div>
</div>
<div class="flex items-center gap-2">
<div class="flex items-center">
<NcDropdown
v-if="!editCommentValue"
class="!hidden !group-hover:block"
overlay-class-name="!min-w-[160px]"
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" />
</NcButton>
<template #overlay>
@ -316,7 +339,7 @@ const editedAt = (comment: CommentType) => {
v-if="user && comment.created_by_email === user.email"
v-e="['c:comment-expand:comment:edit']"
class="text-gray-700"
@click="editComments(comment)"
@click="editComment(comment)"
>
<div class="flex gap-2 items-center">
<component :is="iconMap.rename" class="cursor-pointer" />
@ -349,11 +372,10 @@ const editedAt = (comment: CommentType) => {
</NcMenu>
</template>
</NcDropdown>
<div v-if="appInfo.ee">
<NcTooltip v-if="!comment.resolved_by">
<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"
type="text"
@click="resolveComment(comment.id!)"
@ -366,19 +388,24 @@ const editedAt = (comment: CommentType) => {
<NcTooltip v-else>
<template #title>{{ `Resolved by ${comment.resolved_display_name}` }}</template>
<div class="flex text-[#17803D] font-semibold items-center">
<NcButton class="!h-7 !bg-transparent" size="xsmall" type="text" @click="resolveComment(comment.id)">
<div class="flex items-center gap-2 !text-[#17803D]">
<span> Resolved </span>
<component :is="iconMap.checkCircle" />
</div>
</NcButton>
</div>
<NcButton
class="!h-7 !w-7 !bg-transparent !hover:bg-gray-200 text-semibold"
size="xsmall"
type="text"
@click="resolveComment(comment.id)"
>
<GeneralIcon class="text-md rounded-full bg-[#17803D] text-white" icon="checkFill" />
</NcButton>
</NcTooltip>
</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
v-if="comment.id === editCommentValue?.id"
v-model:value="value"
@ -388,19 +415,19 @@ const editedAt = (comment: CommentType) => {
data-testid="expanded-form-comment-input"
sync-value-change
@save="onEditComment"
@keydown.stop="onKeyDown"
@keydown.esc="onCancel"
@blur="
() => {
editComment = undefined
editCommentValue = undefined
isEditing = false
}
"
@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
:value="comment.comment"
:value="`${comment.comment} ${editedAt(comment)}`"
class="!text-small !leading-18px !text-gray-800 -ml-1"
read-only
sync-value-change
@ -410,7 +437,7 @@ const editedAt = (comment: CommentType) => {
</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
ref="commentInputRef"
v-model:value="comment"
@ -451,8 +478,8 @@ const editedAt = (comment: CommentType) => {
'pb-1': !appInfo.ee,
}"
>
<div v-if="isExpandedFormLoading || isAuditLoading" class="flex flex-col h-full">
<GeneralLoader class="!mt-16" size="xlarge" />
<div v-if="isExpandedFormLoading || isAuditLoading" class="flex flex-col items-center justify-center w-full h-full">
<GeneralLoader size="xlarge" />
</div>
<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;
}
.nc-comment-input {
:deep(.nc-comment-rich-editor) {
@apply !ml-1;
}
.scroll-fix {
flex: 1 1 auto;
}
.nc-audit-item {
@ -534,7 +559,7 @@ const editedAt = (comment: CommentType) => {
:deep(.ant-tabs) {
@apply !overflow-visible;
.ant-tabs-nav {
@apply px-3;
@apply px-3 bg-white;
.ant-tabs-nav-list {
@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) {
@apply !pl-2 bg-white;
@apply bg-white;
}
</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 Underline from '@tiptap/extension-underline'
import Placeholder from '@tiptap/extension-placeholder'
import tippy from 'tippy.js'
import { Link } from '~/helpers/dbTiptapExtensions/links'
const props = withDefaults(
@ -74,10 +75,16 @@ const tiptapExtensions = [
const editor = useEditor({
extensions: tiptapExtensions,
onUpdate: ({ editor }) => {
const markdown = turndownService
.turndown(editor.getHTML().replaceAll(/<p><\/p>/g, '<br />'))
.replaceAll(/\n\n<br \/>\n\n/g, '<br>\n\n')
vModel.value = markdown === '<br />' ? '' : markdown
let markdown = turndownService.turndown(editor.getHTML().replaceAll(/<p><\/p>/g, '<br />'))
const isListsActive = editor?.isActive('bulletList') || editor?.isActive('orderedList') || editor?.isActive('blockquote')
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,
autofocus: props.autofocus,
@ -87,8 +94,12 @@ const editor = useEditor({
onFocusWrapper()
},
onBlur: (e) => {
const targetEl = e?.event.relatedTarget as HTMLElement
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
emits('blur')
@ -142,7 +153,9 @@ useEventListener(
const targetEl = e?.relatedTarget as HTMLElement
if (
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
emits('blur')
@ -155,10 +168,15 @@ useEventListener(
'focusout',
(e: FocusEvent) => {
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
emits('blur')
}
},
@ -169,7 +187,11 @@ onClickOutside(editorDom, (e) => {
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
emits('blur')
}
@ -183,7 +205,7 @@ const emitSave = (event: KeyboardEvent) => {
// If Enter was pressed in the list, do not emit save
triggerSaveFromList.value = false
} 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()
} else {
emits('save')
@ -193,7 +215,8 @@ const emitSave = (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) {
triggerSaveFromList.value = true
setTimeout(() => {
@ -220,6 +243,31 @@ const handleKeyPress = (event: KeyboardEvent) => {
defineExpose({
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>
<template>
@ -249,21 +297,21 @@ defineExpose({
ref="editorDom"
:editor="editor"
:class="{
'px-1.5': !props.readOnly,
'px-2': !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"
/>
<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" />
<NcButton
v-e="['a:row-expand:comment:save']"
: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"
@click="emits('save')"
@click="saveComment"
>
<GeneralIcon icon="send" />
</NcButton>
@ -273,6 +321,10 @@ defineExpose({
</template>
<style lang="scss">
.tooltip {
@apply text-xs bg-gray-800 text-white px-2 py-1 rounded-lg;
}
.nc-rich-text-comment {
.readonly {
.nc-comment-rich-editor {
@ -282,6 +334,11 @@ defineExpose({
}
}
}
.nc-rich-link-tooltip {
@apply text-gray-500;
}
.nc-comment-rich-editor {
&.nc-truncate {
.tiptap.ProseMirror {

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

@ -950,7 +950,7 @@ export default {
: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"
>
<SmartsheetExpandedFormComments :loading="isLoading" />
<SmartsheetExpandedFormComments :primary-key="primaryKey" :loading="isLoading" />
</div>
</div>
</div>

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

@ -19,6 +19,7 @@ export const Link = TiptapLink.extend({
internal: false,
}
},
addAttributes() {
return {
href: {
@ -36,7 +37,17 @@ export const Link = TiptapLink.extend({
renderHTML({ HTMLAttributes }) {
const attr = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)
return ['a', attr, 0]
// 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]
}
// 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() {

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 NcNotification from '~icons/nc-icons/bell'
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 NcPhoneCall from '~icons/nc-icons/phone-call'
@ -405,6 +407,8 @@ export const iconMap = {
search: NcSearch,
calendar: Calendar,
checkCircle: NcCheckCircle,
checkFill: NcCheckFill,
externalLink: NcExternalLink,
error: h('span', { class: 'material-symbols' }, 'error'),
info: h(MsInfo, {}, () => 'info'),
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 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 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.
*/
public async metaUpdate(
@ -596,6 +597,7 @@ export class MetaService {
data: any,
idOrCondition?: string | { [p: string]: any },
xcCondition?: Condition,
skipUpdatedAt = false,
force = false,
): Promise<any> {
const query = this.knexConnection(target);
@ -625,7 +627,10 @@ export class MetaService {
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') {
query.where('id', 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);
}
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(
context: NcContext,
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';
@Injectable()
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) {}

Loading…
Cancel
Save