From 5b968efa2b327c38d8beb033e87e7a3cb7691840 Mon Sep 17 00:00:00 2001 From: Anbarasu Date: Sat, 25 May 2024 11:43:06 +0530 Subject: [PATCH] feat: richtext Comments (#8564) * feat: richtext Comments * fix: minor corrections * fix: minor corrections * fix: minor corrections * fix: update carat color * fix: improved comment upgrader * fix: coderabbit comment fixes * fix: update some behaviours * fix: build * fix: test flaky * fix: test flaky * fix: test failing * fix: remove tasks list. fix: update enter handling using lists * fix: update icons * fix: update dependencies fix: mysql index name long fix: generate API.ts --- packages/nc-gui/assets/nc-icons/at-sign.svg | 11 + .../nc-gui/assets/nc-icons/strike-through.svg | 7 + packages/nc-gui/components/nc/Dropdown.vue | 5 + .../nc-gui/components/smartsheet/Form.vue | 28 +- .../smartsheet/expanded-form/Comments.vue | 295 ++++++++------ .../smartsheet/expanded-form/RichComment.vue | 380 ++++++++++++++++++ .../expanded-form/RichTextOptions.vue | 215 ++++++++++ .../smartsheet/expanded-form/index.vue | 19 +- .../composables/useExpandedFormStore.ts | 145 +++++-- packages/nc-gui/lang/en.json | 1 + packages/nc-gui/package.json | 16 +- packages/nc-gui/utils/datetimeUtils.ts | 26 +- packages/nc-gui/utils/iconUtils.ts | 6 +- packages/nocodb-sdk/src/lib/Api.ts | 252 ++++++++++-- packages/nocodb-sdk/src/lib/enums.ts | 4 + .../src/controllers/audits.controller.ts | 56 +-- .../src/controllers/comments.controller.ts | 90 +++++ .../global-exception.filter.ts | 4 +- packages/nocodb/src/meta/meta.service.ts | 9 + .../meta/migrations/XcMigrationSourcev2.ts | 4 + .../migrations/v2/nc_046_comment_mentions.ts | 142 +++++++ .../extract-ids/extract-ids.middleware.ts | 30 +- packages/nocodb/src/models/Audit.ts | 42 +- packages/nocodb/src/models/Comment.ts | 154 +++++++ packages/nocodb/src/models/Model.ts | 5 +- packages/nocodb/src/models/Notification.ts | 3 + packages/nocodb/src/models/index.ts | 1 + packages/nocodb/src/modules/noco.module.ts | 4 + packages/nocodb/src/schema/swagger-v2.json | 256 ++++++++++-- packages/nocodb/src/schema/swagger.json | 369 +++++++++++++++-- .../services/app-hooks/app-hooks.service.ts | 41 +- .../src/services/app-hooks/interfaces.ts | 18 +- .../nocodb/src/services/audits.service.ts | 52 +-- .../nocodb/src/services/comments.service.ts | 118 ++++++ packages/nocodb/src/utils/acl.ts | 2 + packages/nocodb/src/utils/globals.ts | 3 + packages/nocodb/src/utils/richTextHelper.ts | 17 + pnpm-lock.yaml | 274 ++++++------- .../pages/Dashboard/Details/FieldsPage.ts | 4 +- .../pages/Dashboard/common/Toolbar/index.ts | 5 +- .../tests/db/views/viewCalendar.spec.ts | 3 +- 41 files changed, 2521 insertions(+), 595 deletions(-) create mode 100644 packages/nc-gui/assets/nc-icons/at-sign.svg create mode 100644 packages/nc-gui/assets/nc-icons/strike-through.svg create mode 100644 packages/nc-gui/components/smartsheet/expanded-form/RichComment.vue create mode 100644 packages/nc-gui/components/smartsheet/expanded-form/RichTextOptions.vue create mode 100644 packages/nocodb/src/controllers/comments.controller.ts create mode 100644 packages/nocodb/src/meta/migrations/v2/nc_046_comment_mentions.ts create mode 100644 packages/nocodb/src/models/Comment.ts create mode 100644 packages/nocodb/src/services/comments.service.ts create mode 100644 packages/nocodb/src/utils/richTextHelper.ts diff --git a/packages/nc-gui/assets/nc-icons/at-sign.svg b/packages/nc-gui/assets/nc-icons/at-sign.svg new file mode 100644 index 0000000000..3f371823d7 --- /dev/null +++ b/packages/nc-gui/assets/nc-icons/at-sign.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/nc-gui/assets/nc-icons/strike-through.svg b/packages/nc-gui/assets/nc-icons/strike-through.svg new file mode 100644 index 0000000000..26cff0ca23 --- /dev/null +++ b/packages/nc-gui/assets/nc-icons/strike-through.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/nc-gui/components/nc/Dropdown.vue b/packages/nc-gui/components/nc/Dropdown.vue index f1f4fcae09..e679091fa0 100644 --- a/packages/nc-gui/components/nc/Dropdown.vue +++ b/packages/nc-gui/components/nc/Dropdown.vue @@ -4,11 +4,13 @@ const props = withDefaults( trigger?: Array<'click' | 'hover' | 'contextmenu'> visible?: boolean | undefined overlayClassName?: string | undefined + placement?: 'bottom' | 'top' | 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight' | 'topCenter' | 'bottomCenter' autoClose?: boolean }>(), { trigger: () => ['click'], visible: undefined, + placement: 'bottom', overlayClassName: undefined, autoClose: true, }, @@ -20,6 +22,8 @@ const trigger = toRef(props, 'trigger') const overlayClassName = toRef(props, 'overlayClassName') +const placement = toRef(props, 'placement') + const autoClose = computed(() => props.autoClose) const overlayClassNameComputed = computed(() => { @@ -58,6 +62,7 @@ const onVisibleUpdate = (event: any) => { -
+
- +
{ display: 'inline', }" > - {{ log.display_name?.trim() || log.user || 'Shared source' }} + {{ audit.display_name?.trim() || audit.user || 'Shared source' }} -
- {{ timeAgo(log.created_at) }} +
+ {{ timeAgo(audit.created_at) }}
-
+
@@ -358,6 +388,12 @@ watch(commentsWrapperEl, () => { @apply max-w-1/2; } +.nc-comment-input { + :deep(.nc-comment-rich-editor) { + @apply !ml-1; + } +} + .nc-audit-item { @apply border-b-1 gap-3 border-gray-200; } @@ -386,6 +422,7 @@ watch(commentsWrapperEl, () => { } :deep(.ant-tabs) { + @apply !overflow-visible; .ant-tabs-nav { @apply px-3; .ant-tabs-nav-list { @@ -407,21 +444,11 @@ watch(commentsWrapperEl, () => { } } -.nc-comment-description { - pre { - @apply !mb-0 py-[1px] !text-small !text-gray-700 !leading-18px; - white-space: break-spaces; - font-size: unset; - font-family: unset; - } -} -.expanded-form-comment-input-wrapper { - @apply flex-1 bg-white rounded-lg relative; -} :deep(.expanded-form-comment-input) { - @apply transition-all duration-150; + @apply transition-all duration-150 min-h-8; box-shadow: none; - &:focus { + &:focus, + &:focus-within { @apply min-h-16; } &::placeholder { diff --git a/packages/nc-gui/components/smartsheet/expanded-form/RichComment.vue b/packages/nc-gui/components/smartsheet/expanded-form/RichComment.vue new file mode 100644 index 0000000000..f9b63173bc --- /dev/null +++ b/packages/nc-gui/components/smartsheet/expanded-form/RichComment.vue @@ -0,0 +1,380 @@ + + + + + diff --git a/packages/nc-gui/components/smartsheet/expanded-form/RichTextOptions.vue b/packages/nc-gui/components/smartsheet/expanded-form/RichTextOptions.vue new file mode 100644 index 0000000000..4238e7fea4 --- /dev/null +++ b/packages/nc-gui/components/smartsheet/expanded-form/RichTextOptions.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/packages/nc-gui/components/smartsheet/expanded-form/index.vue b/packages/nc-gui/components/smartsheet/expanded-form/index.vue index 003706f640..6c0dac31e7 100644 --- a/packages/nc-gui/components/smartsheet/expanded-form/index.vue +++ b/packages/nc-gui/components/smartsheet/expanded-form/index.vue @@ -129,7 +129,8 @@ const { saveRowAndStay, row: _row, save: _save, - loadCommentsAndLogs, + loadComments, + loadAudits, clearColumns, } = useProvideExpandedFormStore(meta, row) @@ -320,13 +321,13 @@ onMounted(async () => { if (props.loadRow) { await _loadRow() - await loadCommentsAndLogs() + await Promise.all([loadComments(), loadAudits()]) } if (props.rowId) { try { await _loadRow(props.rowId) - await loadCommentsAndLogs() + await Promise.all([loadComments(), loadAudits()]) } catch (e: any) { if (e.response?.status === 404) { message.error(t('msg.noRecordFound')) @@ -458,7 +459,7 @@ const onConfirmDeleteRowClick = async () => { watch(rowId, async (nRow) => { await _loadRow(nRow) - await loadCommentsAndLogs() + await Promise.all([loadComments(), loadAudits()]) }) const showRightSections = computed(() => { @@ -617,7 +618,7 @@ export default { {{ props.newRecordHeader ?? $t('activity.newRecord') }}
@@ -720,7 +721,7 @@ export default {
-
+
@@ -981,10 +982,6 @@ export default { .nc-drawer-expanded-form { @apply xs:my-0; - .ant-modal-content { - @apply overflow-hidden; - } - .ant-drawer-content-wrapper { @apply !h-[90vh]; .ant-drawer-content { diff --git a/packages/nc-gui/composables/useExpandedFormStore.ts b/packages/nc-gui/composables/useExpandedFormStore.ts index b2c773055a..03df720bf7 100644 --- a/packages/nc-gui/composables/useExpandedFormStore.ts +++ b/packages/nc-gui/composables/useExpandedFormStore.ts @@ -1,4 +1,4 @@ -import type { AuditType, ColumnType, TableType } from 'nocodb-sdk' +import type { AuditType, ColumnType, CommentType, TableType } from 'nocodb-sdk' import { UITypes, ViewTypes, isVirtualCol } from 'nocodb-sdk' import type { Ref } from 'vue' import dayjs from 'dayjs' @@ -6,15 +6,23 @@ import dayjs from 'dayjs' const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((meta: Ref, _row: Ref) => { const { $e, $state, $api } = useNuxtApp() - const { api, isLoading: isCommentsLoading, error: commentsError } = useApi() - const { t } = useI18n() const isPublic = inject(IsPublicInj, ref(false)) - const commentsOnly = ref(false) + const comments = ref< + Array< + CommentType & { + created_display_name: string + } + > + >([]) + + const audits = ref>([]) - const commentsAndLogs = ref([]) + const isCommentsLoading = ref(false) + + const isAuditLoading = ref(false) const comment = ref('') @@ -24,8 +32,14 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m const changedColumns = ref(new Set()) + const basesStore = useBases() + + const { basesUser } = storeToRefs(basesStore) + const { base } = storeToRefs(useBase()) + const baseUsers = computed(() => (meta.value.base_id ? basesUser.value.get(meta.value.base_id) || [] : [])) + const { sharedView } = useSharedView() const row = ref( @@ -87,24 +101,73 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m return extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]) }) - // actions - const loadCommentsAndLogs = async () => { - if (!isUIAllowed('commentList')) return - - if (!row.value) return + const loadComments = async () => { + if (!isUIAllowed('commentList') || !row.value) return const rowId = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]) if (!rowId) return - commentsAndLogs.value = - ( - await api.utils.commentList({ + try { + isCommentsLoading.value = true + + const res = (( + await $api.utils.commentList({ row_id: rowId, fk_model_id: meta.value.id as string, - comments_only: commentsOnly.value, }) - ).list?.reverse?.() || [] + ).list || []) as Array< + CommentType & { + created_display_name: string + } + > + + comments.value = res.map((comment) => { + const user = baseUsers.value.find((u) => u.id === comment.created_by) + return { + ...comment, + created_display_name: user?.display_name ?? (user?.email ?? '').split('@')[0], + } + }) + } catch (e: any) { + message.error(e.message) + } finally { + isCommentsLoading.value = false + } + } + + const deleteComment = async (commentId: string) => { + if (!isUIAllowed('commentDelete')) return + + try { + await $api.utils.commentDelete(commentId) + await loadComments() + } catch (e: any) { + message.error(e.message) + } + } + + const loadAudits = async () => { + if (!isUIAllowed('auditList') || !row.value) return + + const rowId = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]) + + if (!rowId) return + + try { + isAuditLoading.value = true + audits.value = + ( + await $api.utils.auditList({ + row_id: rowId, + fk_model_id: meta.value.id as string, + }) + ).list?.reverse?.() || [] + } catch (e: any) { + message.error(e.message) + } finally { + isAuditLoading.value = false + } } const isYou = (email: string) => { @@ -126,15 +189,15 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m if (!rowId) return - await api.utils.commentRow({ + await $api.utils.commentRow({ fk_model_id: meta.value?.id as string, row_id: rowId, - description: `The following comment has been created: ${comment.value}`, + comment: `${comment.value}`, }) reloadTrigger?.trigger() - await loadCommentsAndLogs() + await Promise.all([loadComments(), loadAudits()]) comment.value = '' } catch (e: any) { @@ -154,6 +217,8 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m kanbanClbk?: (row: Row, isNewRow: boolean) => void } = {}, ) => { + if (!meta.value.id) return + let data const isNewRow = row.value.rowMeta?.new ?? false @@ -222,6 +287,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m obj[col] = row.value.row[col] return obj }, {} as Record) + if (Object.keys(updateOrInsertObj).length) { const id = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]) @@ -240,7 +306,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m addUndo({ redo: { fn: async (id: string, data: Record) => { - await $api.dbTableRow.update(NOCO, base.value.id as string, meta.value.id, encodeURIComponent(id), data) + await $api.dbTableRow.update(NOCO, base.value.id as string, meta.value.id!, encodeURIComponent(id), data) await loadKanbanData() reloadTrigger?.trigger() @@ -249,7 +315,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m }, undo: { fn: async (id: string, data: Record) => { - await $api.dbTableRow.update(NOCO, base.value.id as string, meta.value.id, encodeURIComponent(id), data) + await $api.dbTableRow.update(NOCO, base.value.id as string, meta.value.id!, encodeURIComponent(id), data) await loadKanbanData() reloadTrigger?.trigger() }, @@ -260,7 +326,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m } if (commentsDrawer.value) { - await loadCommentsAndLogs() + await Promise.all([loadComments(), loadAudits()]) } } else { // No columns to update @@ -283,15 +349,18 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m } const loadRow = async (rowId?: string, onlyVirtual = false, onlyNewColumns = false) => { - if (row.value.rowMeta.new) return + if (row.value.rowMeta.new || isPublic.value || !meta.value?.id) return + + const recordId = rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]) + + if (!recordId) return - if (isPublic.value || !meta.value?.id) return let record = await $api.dbTableRow.read( NOCO, // todo: base_id missing on view type - (base?.value?.id || (sharedView.value?.view as any)?.base_id) as string, + ((base?.value?.id ?? meta.value?.base_id) || (sharedView.value?.view as any)?.base_id) as string, meta.value.id as string, - encodeURIComponent(rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])), + encodeURIComponent(recordId), { getHiddenColumn: true, }, @@ -301,9 +370,9 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m if (onlyVirtual) { record = { ...row.value.row, - ...meta.value.columns.reduce((partialRecord, col) => { - if (isVirtualCol(col) && col.title in record) { - partialRecord[col.title] = record[col.title] + ...(meta.value.columns ?? []).reduce((partialRecord, col) => { + if (isVirtualCol(col) && col.title && col.title in record) { + partialRecord[col.title] = (record as Record)[col.title as string] } return partialRecord }, {} as Record), @@ -314,7 +383,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m if (onlyNewColumns) { record = Object.keys(record).reduce((acc, curr) => { if (!Object.prototype.hasOwnProperty.call(row.value.row, curr)) { - acc[curr] = record[curr] + acc[curr] = record(record as Record)[curr] } else { acc[curr] = row.value.row[curr] } @@ -331,11 +400,13 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m const deleteRowById = async (rowId?: string) => { try { + const recordId = rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]) + const res: { message?: string[] } | number = await $api.dbTableRow.delete( NOCO, base.value.id as string, meta.value.id as string, - encodeURIComponent(rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])), + encodeURIComponent(recordId), ) if (res.message) { @@ -351,17 +422,19 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m } } - const updateComment = async (auditId: string, audit: Partial) => { - return await $api.utils.commentUpdate(auditId, audit) + const updateComment = async (commentId: string, comment: Partial) => { + return await $api.utils.commentUpdate(commentId, comment) } return { ...rowStore, - commentsOnly, - loadCommentsAndLogs, - commentsAndLogs, + loadComments, + deleteComment, + loadAudits, + comments, + audits, + isAuditLoading, isCommentsLoading, - commentsError, saveComment, comment, isYou, diff --git a/packages/nc-gui/lang/en.json b/packages/nc-gui/lang/en.json index 2d16cdc082..012e80c302 100644 --- a/packages/nc-gui/lang/en.json +++ b/packages/nc-gui/lang/en.json @@ -451,6 +451,7 @@ "labels": { "connectionDetails": "Connection Details", "metaSync": "Meta Sync", + "mention": "Mention", "today": "Today", "workspace": "Workspace", "txt": "TXT Record value", diff --git a/packages/nc-gui/package.json b/packages/nc-gui/package.json index d656225251..f8ffb2f7a1 100644 --- a/packages/nc-gui/package.json +++ b/packages/nc-gui/package.json @@ -41,14 +41,14 @@ "@iconify/vue": "^4.1.2", "@nuxt/image": "^1.3.0", "@pinia/nuxt": "^0.5.1", - "@tiptap/extension-link": "2.2.6", - "@tiptap/extension-placeholder": "^2.2.6", - "@tiptap/extension-task-list": "2.2.6", - "@tiptap/extension-underline": "^2.2.6", - "@tiptap/html": "2.2.6", - "@tiptap/pm": "^2.2.6", - "@tiptap/starter-kit": "^2.2.6", - "@tiptap/vue-3": "2.2.6", + "@tiptap/extension-link": "^2.4.0", + "@tiptap/extension-placeholder": "^2.4.0", + "@tiptap/extension-task-list": "2.4.0", + "@tiptap/extension-underline": "^2.4.0", + "@tiptap/html": "2.4.0", + "@tiptap/pm": "^2.4.0", + "@tiptap/starter-kit": "^2.4.0", + "@tiptap/vue-3": "2.4.0", "@vue-flow/additional-components": "^1.3.3", "@vue-flow/core": "^1.30.1", "@vuelidate/core": "^2.0.3", diff --git a/packages/nc-gui/utils/datetimeUtils.ts b/packages/nc-gui/utils/datetimeUtils.ts index 7b0979ac66..8555788b6d 100644 --- a/packages/nc-gui/utils/datetimeUtils.ts +++ b/packages/nc-gui/utils/datetimeUtils.ts @@ -24,5 +24,29 @@ export const timeAgo = (date: any) => { date += '+00:00' } // show in local time - return dayjs(date).fromNow() + const diff = dayjs().diff(date) + + const seconds = Math.floor(diff / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + const months = Math.floor(days / 30) + const years = Math.floor(days / 365) + + if (seconds < 60) { + return `${seconds}s ago` + } + if (minutes < 60) { + return `${minutes}m ago` + } + if (hours < 24) { + return `${hours}h ago` + } + if (days < 30) { + return `${days}d ago` + } + if (months < 12) { + return `${months}mo ago` + } + return `${years}y ago` } diff --git a/packages/nc-gui/utils/iconUtils.ts b/packages/nc-gui/utils/iconUtils.ts index a22f3de21d..c2597a20d0 100644 --- a/packages/nc-gui/utils/iconUtils.ts +++ b/packages/nc-gui/utils/iconUtils.ts @@ -136,8 +136,10 @@ import NcPhoneCall from '~icons/nc-icons/phone-call' import NcItalic from '~icons/nc-icons/italic' import NcBold from '~icons/nc-icons/bold' import NcUnderline from '~icons/nc-icons/underline' -import NcCrop from '~icons/nc-icons/crop' import NcLink from '~icons/nc-icons/link' +import NcAtSign from '~icons/nc-icons/at-sign' +import NcStrike from '~icons/nc-icons/strike-through' +import NcCrop from '~icons/nc-icons/crop' import NcControlPanel from '~icons/nc-icons/control-panel' import NcHome from '~icons/nc-icons/home' import NcWorkspace from '~icons/nc-icons/workspace' @@ -337,6 +339,8 @@ import NcMessageCircle from '~icons/nc-icons/message-circle' } as const */ export const iconMap = { + strike: NcStrike, + atSign: NcAtSign, slash: NcSlash, arrowUpRight: NcArrowUpRight, ncWorkspace: NcWorkspace, diff --git a/packages/nocodb-sdk/src/lib/Api.ts b/packages/nocodb-sdk/src/lib/Api.ts index 3209523455..8a4e1fd8eb 100644 --- a/packages/nocodb-sdk/src/lib/Api.ts +++ b/packages/nocodb-sdk/src/lib/Api.ts @@ -540,7 +540,7 @@ export interface CommentReqType { * Description for the target row * @example This is the comment for the row */ - description?: string; + comment?: string; /** * Foreign Key to Model * @example md_ehn5izr99m7d45 @@ -561,7 +561,12 @@ export interface CommentUpdateReqType { * Description for the target row * @example This is the comment for the row */ - description?: string; + comment?: string; + /** + * Foreign Key to Model + * @example md_ehn5izr99m7d45 + */ + fk_model_id?: string; } /** @@ -2963,6 +2968,144 @@ export interface CalendarColumnReqType { order?: number; } +/** + * Model for Comment + */ +export interface CommentType { + /** Unique ID */ + id?: IdType; + /** + * Row ID + * @example rec0Adp9PMG9o7uJy + */ + row_id?: string; + /** + * Comment + * @example This is a comment + */ + comment?: string; + /** + * Created By User ID + * @example usr0Adp9PMG9o7uJy + */ + created_by?: IdType; + /** + * Created By User Email + * @example xxx@nocodb.com + */ + created_by_email?: string; + /** + * Resolved By User ID + * @example usr0Adp9PMG9o7uJy + */ + resolved_by?: IdType; + /** + * Resolved By User Email + * @example xxx@nocodb.com + */ + resolved_by_email?: string; + /** + * Parent Comment ID + * @example cmt043cx4r30343ff + */ + parent_comment_id?: IdType; + /** + * Source ID + * @example src0Adp9PMG9o7uJy + */ + source_id?: IdType; + /** + * Base ID + * @example bas0Adp9PMG9o7uJy + */ + base_id?: IdType; + /** + * Model ID + * @example mod0Adp9PMG9o7uJy + */ + fk_model_id?: IdType; + /** + * Created At + * @example 2020-05-20T12:00:00.000000Z + */ + created_at?: string; + /** + * Updated At + * @example 2020-05-20T12:00:00.000000Z + */ + updated_at?: string; + /** Whether the comment has been deleted by the user or not */ + is_deleted?: boolean; +} + +/** + * Model for User Comment Notification Preference + */ +export interface UserCommentNotificationPreferenceType { + /** Unique ID */ + id?: IdType; + /** User ID */ + row_id?: string; + /** User ID */ + user_id?: IdType; + /** + * Source ID + * @example src0Adp9PMG9o7uJy + */ + source_id?: IdType; + /** + * Base ID + * @example bas0Adp9PMG9o7uJy + */ + base_id?: IdType; + /** + * Model ID + * @example mod0Adp9PMG9o7uJy + */ + fk_model_id?: IdType; + /** Is Read */ + preference?: 'ALL_COMMENTS' | 'ONLY_MENTIONS'; + /** Created At */ + created_at?: string; + /** Updated At */ + updated_at?: string; +} + +/** + * Model for Comment Reactions + */ +export interface CommentReactionsType { + /** Unique ID */ + id?: IdType; + /** Row ID */ + row_id?: string; + /** Comment ID */ + comment_id?: IdType; + /** Reaction */ + reaction?: string; + /** User ID */ + user_id?: IdType; + /** + * Source ID + * @example src0Adp9PMG9o7uJy + */ + source_id?: IdType; + /** + * Base ID + * @example bas0Adp9PMG9o7uJy + */ + base_id?: IdType; + /** + * Model ID + * @example mod0Adp9PMG9o7uJy + */ + fk_model_id?: IdType; + /** Created At */ + created_at?: string; + /** Updated At */ + updated_at?: string; +} + export interface ExtensionType { /** Unique ID */ id?: IdType; @@ -9671,12 +9814,12 @@ export class Api< }; utils = { /** - * @description List all comments + * @description List all audits * * @tags Utils - * @name CommentList - * @summary List Comments in Audit - * @request GET:/api/v1/db/meta/audits/comments + * @name AuditList + * @summary List Audits + * @request GET:/api/v1/db/meta/audits * @response `200` `{ list: (AuditType)[], @@ -9687,7 +9830,7 @@ export class Api< }` */ - commentList: ( + auditList: ( query: { /** * Row ID @@ -9699,24 +9842,67 @@ export class Api< * @example md_c6csq89tl37jm5 */ fk_model_id: IdType; + }, + params: RequestParams = {} + ) => + this.request< + { + list: AuditType[]; + }, + { + /** @example BadRequest [Error]: */ + msg: string; + } + >({ + path: `/api/v1/db/meta/audits`, + method: 'GET', + query: query, + format: 'json', + ...params, + }), + + /** + * @description List all comments + * + * @tags Utils + * @name CommentList + * @summary List Comments + * @request GET:/api/v1/db/meta/comments + * @response `200` `{ + list: (CommentType)[], + +}` OK + * @response `400` `{ + \** @example BadRequest [Error]: *\ + msg: string, + +}` + */ + commentList: ( + query: { /** - * Is showing comments only? - * @example true + * Row ID + * @example 10 + */ + row_id: string; + /** + * Foreign Key to Model + * @example md_c6csq89tl37jm5 */ - comments_only?: boolean; + fk_model_id: IdType; }, params: RequestParams = {} ) => this.request< { - list: AuditType[]; + list: CommentType[]; }, { /** @example BadRequest [Error]: */ msg: string; } >({ - path: `/api/v1/db/meta/audits/comments`, + path: `/api/v1/db/meta/comments`, method: 'GET', query: query, format: 'json', @@ -9724,13 +9910,13 @@ export class Api< }), /** - * @description Create a new comment in a row. Logged in Audit. + * @description Create a new comment in a row. * * @tags Utils * @name CommentRow * @summary Comment Rows - * @request POST:/api/v1/db/meta/audits/comments - * @response `200` `AuditType` OK + * @request POST:/api/v1/db/meta/comments + * @response `200` `CommentType` OK * @response `400` `{ \** @example BadRequest [Error]: *\ msg: string, @@ -9739,13 +9925,13 @@ export class Api< */ commentRow: (data: CommentReqType, params: RequestParams = {}) => this.request< - AuditType, + CommentType, { /** @example BadRequest [Error]: */ msg: string; } >({ - path: `/api/v1/db/meta/audits/comments`, + path: `/api/v1/db/meta/comments`, method: 'POST', body: data, type: ContentType.Json, @@ -9754,21 +9940,21 @@ export class Api< }), /** - * @description Update comment in Audit + * @description Update comment * * @tags Utils * @name CommentUpdate - * @summary Update Comment in Audit - * @request PATCH:/api/v1/db/meta/audits/{auditId}/comment + * @summary Update Comment + * @request PATCH:/api/v1/db/meta/comment/{commentId}/ * @response `200` `number` OK */ commentUpdate: ( - auditId: string, + commentId: string, data: CommentUpdateReqType, params: RequestParams = {} ) => this.request({ - path: `/api/v1/db/meta/audits/${auditId}/comment`, + path: `/api/v1/db/meta/comment/${commentId}/`, method: 'PATCH', body: data, type: ContentType.Json, @@ -9777,12 +9963,30 @@ export class Api< }), /** + * @description Delete comment + * + * @tags Utils + * @name CommentDelete + * @summary Delete Comment + * @request DELETE:/api/v1/db/meta/comment/{commentId}/ + * @response `200` `number` OK + */ + commentDelete: (commentId: string, data: any, params: RequestParams = {}) => + this.request({ + path: `/api/v1/db/meta/comment/${commentId}/`, + method: 'DELETE', + body: data, + format: 'json', + ...params, + }), + + /** * @description Return the number of comments in the given query. * * @tags Utils * @name CommentCount * @summary Count Comments - * @request GET:/api/v1/db/meta/audits/comments/count + * @request GET:/api/v1/db/meta/comments/count * @response `200` `({ \** * The number of comments @@ -9829,7 +10033,7 @@ export class Api< msg: string; } >({ - path: `/api/v1/db/meta/audits/comments/count`, + path: `/api/v1/db/meta/comments/count`, method: 'GET', query: query, format: 'json', diff --git a/packages/nocodb-sdk/src/lib/enums.ts b/packages/nocodb-sdk/src/lib/enums.ts index 1321b1b460..646a3ee7b2 100644 --- a/packages/nocodb-sdk/src/lib/enums.ts +++ b/packages/nocodb-sdk/src/lib/enums.ts @@ -147,6 +147,10 @@ export enum AppEvents { EXTENSION_CREATE = 'extension.create', EXTENSION_UPDATE = 'extension.update', EXTENSION_DELETE = 'extension.delete', + + COMMENT_CREATE = 'comment.create', + COMMENT_DELETE = 'comment.delete', + COMMENT_UPDATE = 'comment.update', } export enum ClickhouseTables { diff --git a/packages/nocodb/src/controllers/audits.controller.ts b/packages/nocodb/src/controllers/audits.controller.ts index 64791f5928..41f059e6a7 100644 --- a/packages/nocodb/src/controllers/audits.controller.ts +++ b/packages/nocodb/src/controllers/audits.controller.ts @@ -4,9 +4,7 @@ import { Get, HttpCode, Param, - Patch, Post, - Query, Req, UseGuards, } from '@nestjs/common'; @@ -22,14 +20,12 @@ import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard'; export class AuditsController { constructor(private readonly auditsService: AuditsService) {} - @Post(['/api/v1/db/meta/audits/comments', '/api/v2/meta/audits/comments']) - @HttpCode(200) - @Acl('commentRow') - async commentRow(@Req() req: Request) { - return await this.auditsService.commentRow({ - user: (req as any).user, - body: req.body, - }); + @Get(['/api/v1/db/meta/audits/', '/api/v2/meta/audits/']) + @Acl('auditList') + async auditListRow(@Req() req: Request) { + return new PagedResponseImpl( + await this.auditsService.auditOnlyList({ query: req.query as any }), + ); } @Post([ @@ -45,31 +41,6 @@ export class AuditsController { }); } - @Get(['/api/v1/db/meta/audits/comments', '/api/v2/meta/audits/comments']) - @Acl('commentList') - async commentList(@Req() req: Request) { - return new PagedResponseImpl( - await this.auditsService.commentList({ query: req.query }), - ); - } - - @Patch([ - '/api/v1/db/meta/audits/:auditId/comment', - '/api/v2/meta/audits/:auditId/comment', - ]) - @Acl('commentUpdate') - async commentUpdate( - @Param('auditId') auditId: string, - @Req() req: Request, - @Body() body: any, - ) { - return await this.auditsService.commentUpdate({ - auditId, - userEmail: req.user?.email, - body: body, - }); - } - @Get([ '/api/v1/db/meta/projects/:baseId/audits/', '/api/v2/meta/bases/:baseId/audits/', @@ -87,19 +58,4 @@ export class AuditsController { }, ); } - - @Get([ - '/api/v1/db/meta/audits/comments/count', - '/api/v2/meta/audits/comments/count', - ]) - @Acl('commentsCount') - async commentsCount( - @Query('fk_model_id') fk_model_id: string, - @Query('ids') ids: string[], - ) { - return await this.auditsService.commentsCount({ - fk_model_id, - ids, - }); - } } diff --git a/packages/nocodb/src/controllers/comments.controller.ts b/packages/nocodb/src/controllers/comments.controller.ts new file mode 100644 index 0000000000..b5889baa1b --- /dev/null +++ b/packages/nocodb/src/controllers/comments.controller.ts @@ -0,0 +1,90 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + Param, + Patch, + Post, + Query, + Req, + UseGuards, +} from '@nestjs/common'; +import { Request } from 'express'; +import { GlobalGuard } from '~/guards/global/global.guard'; +import { PagedResponseImpl } from '~/helpers/PagedResponse'; +import { CommentsService } from '~/services/comments.service'; +import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; +import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard'; +import { NcRequest } from '~/interface/config'; + +@Controller() +@UseGuards(MetaApiLimiterGuard, GlobalGuard) +export class CommentsController { + constructor(private readonly commentsService: CommentsService) {} + + @Get(['/api/v1/db/meta/comments', '/api/v2/meta/comments']) + @Acl('commentList') + async commentList(@Req() req: any) { + return new PagedResponseImpl( + await this.commentsService.commentList({ query: req.query }), + ); + } + + @Post(['/api/v1/db/meta/comments', '/api/v2/meta/comments']) + @HttpCode(200) + @Acl('commentRow') + async commentRow(@Req() req: NcRequest, @Body() body: any) { + return await this.commentsService.commentRow({ + user: req.user, + body: body, + req, + }); + } + + @Delete([ + '/api/v1/db/meta/comment/:commentId', + '/api/v2/meta/comment/:commentId', + ]) + @Acl('commentDelete') + async commentDelete( + @Req() req: NcRequest, + @Param('commentId') commentId: string, + ) { + return await this.commentsService.commentDelete({ + commentId, + user: req.user, + }); + } + + @Patch([ + '/api/v1/db/meta/comment/:commentId', + '/api/v2/meta/comment/:commentId', + ]) + @Acl('commentUpdate') + async commentUpdate( + @Param('commentId') commentId: string, + @Req() req: any, + @Body() body: any, + ) { + return await this.commentsService.commentUpdate({ + commentId: commentId, + user: req.user, + body: body, + req, + }); + } + + @Get(['/api/v1/db/meta/comments/count', '/api/v2/meta/comments/count']) + @Acl('commentsCount') + async commentsCount( + @Query('fk_model_id') fk_model_id: string, + @Query('ids') ids: string[], + ) { + return await this.commentsService.commentsCount({ + fk_model_id, + ids, + }); + } +} diff --git a/packages/nocodb/src/filters/global-exception/global-exception.filter.ts b/packages/nocodb/src/filters/global-exception/global-exception.filter.ts index f3e275c0c4..2c4a7e4c46 100644 --- a/packages/nocodb/src/filters/global-exception/global-exception.filter.ts +++ b/packages/nocodb/src/filters/global-exception/global-exception.filter.ts @@ -41,7 +41,9 @@ export class GlobalExceptionFilter implements ExceptionFilter { } // try to extract db error for unknown errors - const dbError = !(exception instanceof NcBaseError) ? extractDBError(exception) : null; + const dbError = !(exception instanceof NcBaseError) + ? extractDBError(exception) + : null; // skip unnecessary error logging if ( diff --git a/packages/nocodb/src/meta/meta.service.ts b/packages/nocodb/src/meta/meta.service.ts index 55dac6706b..758c4a7934 100644 --- a/packages/nocodb/src/meta/meta.service.ts +++ b/packages/nocodb/src/meta/meta.service.ts @@ -252,6 +252,15 @@ export class MetaService { case MetaTable.EXTENSIONS: prefix = 'ext'; break; + case MetaTable.COMMENTS: + prefix = 'com'; + break; + case MetaTable.COMMENTS_REACTIONS: + prefix = 'cre'; + break; + case MetaTable.USER_COMMENTS_NOTIFICATIONS_PREFERENCE: + prefix = 'cnp'; + break; default: prefix = 'nc'; break; diff --git a/packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts b/packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts index 711122669d..22c21739db 100644 --- a/packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts +++ b/packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts @@ -32,6 +32,7 @@ import * as nc_042_user_block from '~/meta/migrations/v2/nc_042_user_block'; import * as nc_043_user_refresh_token from '~/meta/migrations/v2/nc_043_user_refresh_token'; import * as nc_044_view_column_index from '~/meta/migrations/v2/nc_044_view_column_index'; import * as nc_045_extensions from '~/meta/migrations/v2/nc_045_extensions'; +import * as nc_046_comment_mentions from '~/meta/migrations/v2/nc_046_comment_mentions'; // Create a custom migration source class export default class XcMigrationSourcev2 { @@ -75,6 +76,7 @@ export default class XcMigrationSourcev2 { 'nc_043_user_refresh_token', 'nc_044_view_column_index', 'nc_045_extensions', + 'nc_046_comment_mentions', ]); } @@ -152,6 +154,8 @@ export default class XcMigrationSourcev2 { return nc_044_view_column_index; case 'nc_045_extensions': return nc_045_extensions; + case 'nc_046_comment_mentions': + return nc_046_comment_mentions; } } } diff --git a/packages/nocodb/src/meta/migrations/v2/nc_046_comment_mentions.ts b/packages/nocodb/src/meta/migrations/v2/nc_046_comment_mentions.ts new file mode 100644 index 0000000000..653f3be43a --- /dev/null +++ b/packages/nocodb/src/meta/migrations/v2/nc_046_comment_mentions.ts @@ -0,0 +1,142 @@ +import { Logger } from '@nestjs/common'; +import type { Knex } from 'knex'; +import { MetaTable } from '~/utils/globals'; + +const logger = new Logger('nc_046_comment_mentions'); + +const up = async (knex: Knex) => { + await knex.schema.createTable(MetaTable.COMMENTS, (table) => { + table.string('id', 20).primary(); + + table.string('row_id', 255); + + table.string('comment', 3000); + + table.string('created_by', 255); + + table.string('created_by_email', 255); + + table.string('resolved_by', 255); + + table.string('resolved_by_email', 255); + + table.string('parent_comment_id', 20); + + table.string('source_id', 20); + + table.string('base_id', 128); + + table.string('fk_model_id', 20); + + table.boolean('is_deleted'); + + table.index(['row_id', 'fk_model_id']); + + table.timestamps(true, true); + }); + + await knex.schema.createTable( + MetaTable.USER_COMMENTS_NOTIFICATIONS_PREFERENCE, + (table) => { + table.string('id', 20).primary(); + + table.string('row_id', 255); + + table.string('user_id', 255); + + table.string('fk_model_id', 20); + + table.string('source_id', 20); + + table.string('base_id', 128); + + table.string('preferences', 255); + + table.index( + ['user_id', 'row_id', 'fk_model_id'], + 'user_comments_preference_index', + ); + + table.timestamps(true, true); + }, + ); + + await knex.schema.createTable(MetaTable.COMMENTS_REACTIONS, (table) => { + table.string('id', 20).primary(); + + table.string('row_id', 255).index(); + + table.string('comment_id', 20).index(); + + table.string('source_id', 20); + + table.string('fk_modal_id', 20); + + table.string('base_id', 128); + + table.string('reaction', 255); + + table.string('created_by', 255); + + table.timestamps(true, true); + }); + + logger.log('nc_046_comment_mentions: Tables Created'); + + knex + .select( + `${MetaTable.AUDIT}.id`, + `${MetaTable.AUDIT}.row_id`, + `${MetaTable.AUDIT}.description`, + `${MetaTable.AUDIT}.user as user_email`, + `${MetaTable.AUDIT}.source_id`, + `${MetaTable.AUDIT}.base_id`, + `${MetaTable.AUDIT}.fk_model_id`, + `${MetaTable.AUDIT}.created_at`, + `${MetaTable.AUDIT}.updated_at`, + `${MetaTable.USERS}.id as user_id`, + ) + .from(MetaTable.AUDIT) + .where(`${MetaTable.AUDIT}.op_type`, 'COMMENT') + .leftJoin( + MetaTable.USERS, + `${MetaTable.AUDIT}.user`, + `${MetaTable.USERS}.email`, + ) + .then(async (rows) => { + if (!rows.length) return; + + logger.log('nc_046_comment_mentions: Data from Audit Table Selected'); + + const formattedRows = rows.map((row) => ({ + id: row.id, + row_id: row.row_id, + comment: row.description + .substring(row.description.indexOf(':') + 1) + .trim(), + created_by: row.user_id, + created_by_email: row.user_email, + source_id: row.source_id, + base_id: row.base_id, + fk_model_id: row.fk_model_id, + created_at: row.created_at, + updated_at: row.updated_at, + })); + + logger.log('nc_046_comment_mentions: Data from Audit Table Formatted'); + + await knex(MetaTable.COMMENTS).insert(formattedRows); + + logger.log('nc_046_comment_mentions: Data from Audit Table Migrated'); + }); +}; + +const down = async (knex: Knex) => { + await knex.schema.dropTable(MetaTable.COMMENTS); + + await knex.schema.dropTable(MetaTable.USER_COMMENTS_NOTIFICATIONS_PREFERENCE); + + await knex.schema.dropTable(MetaTable.COMMENTS_REACTIONS); +}; + +export { up, down }; diff --git a/packages/nocodb/src/middlewares/extract-ids/extract-ids.middleware.ts b/packages/nocodb/src/middlewares/extract-ids/extract-ids.middleware.ts index ff8c3bdc76..a9246d893d 100644 --- a/packages/nocodb/src/middlewares/extract-ids/extract-ids.middleware.ts +++ b/packages/nocodb/src/middlewares/extract-ids/extract-ids.middleware.ts @@ -11,9 +11,9 @@ import type { NestMiddleware, } from '@nestjs/common'; import { - Audit, Base, Column, + Comment, Extension, Filter, FormViewColumn, @@ -135,9 +135,9 @@ export class ExtractIdsMiddleware implements NestMiddleware, CanActivate { else if ( [ '/api/v1/db/meta/audits/rows/:rowId/update', - '/api/v1/db/meta/audits/comments', '/api/v2/meta/audits/rows/:rowId/update', - '/api/v2/meta/audits/comments', + '/api/v1/db/meta/comments', + '/api/v2/meta/comments', ].some( (auditInsertOrUpdatePath) => req.route.path === auditInsertOrUpdatePath, ) && @@ -152,10 +152,12 @@ export class ExtractIdsMiddleware implements NestMiddleware, CanActivate { // extract fk_model_id from query params only if it's audit get endpoint else if ( [ - '/api/v1/db/meta/audits/comments/count', - '/api/v1/db/meta/audits/comments', - '/api/v2/meta/audits/comments/count', - '/api/v2/meta/audits/comments', + '/api/v2/meta/comments/count', + '/api/v1/db/meta/comments/count', + '/api/v2/meta/comments', + '/api/v1/db/meta/comments', + '/api/v1/db/meta/audits', + '/api/v2/meta/audits', ].some((auditReadPath) => req.route.path === auditReadPath) && req.method === 'GET' && req.query.fk_model_id @@ -166,14 +168,14 @@ export class ExtractIdsMiddleware implements NestMiddleware, CanActivate { req.ncBaseId = model?.base_id; } else if ( [ - '/api/v1/db/meta/audits/:auditId/comment', - '/api/v2/meta/audits/:auditId/comment', - ].some((auditPatchPath) => req.route.path === auditPatchPath) && - req.method === 'PATCH' && - req.params.auditId + '/api/v1/db/meta/comment/:commentId', + '/api/v2/meta/comment/:commentId', + ].some((commentPatchPath) => req.route.path === commentPatchPath) && + (req.method === 'PATCH' || req.method === 'DELETE') && + req.params.commentId ) { - const audit = await Audit.get(params.auditId); - req.ncBaseId = audit?.base_id; + const comment = await Comment.get(params.commentId); + req.ncBaseId = comment?.base_id; } // extract base id from query params only if it's userMe endpoint or webhook plugin list else if ( diff --git a/packages/nocodb/src/models/Audit.ts b/packages/nocodb/src/models/Audit.ts index 214ba280ec..4ecc03b4a7 100644 --- a/packages/nocodb/src/models/Audit.ts +++ b/packages/nocodb/src/models/Audit.ts @@ -122,22 +122,7 @@ export default class Audit implements AuditType { } } - public static async commentsCount(args: { - ids: string[]; - fk_model_id: string; - }) { - const audits = await Noco.ncMeta - .knex(MetaTable.AUDIT) - .count('id', { as: 'count' }) - .select('row_id') - .whereIn('row_id', args.ids) - .where('fk_model_id', args.fk_model_id) - .where('op_type', AuditOperationTypes.COMMENT) - .groupBy('row_id'); - - return audits?.map((a) => new Audit(a)); - } - public static async commentsList(args) { + public static async auditList(args) { const query = Noco.ncMeta .knex(MetaTable.AUDIT) .join( @@ -148,11 +133,9 @@ export default class Audit implements AuditType { .select(`${MetaTable.AUDIT}.*`, `${MetaTable.USERS}.display_name`) .where('row_id', args.row_id) .where('fk_model_id', args.fk_model_id) + .where('op_type', '!=', AuditOperationTypes.COMMENT) .orderBy('created_at', 'desc'); - if ((args.comments_only as any) == 'true') - query.where('op_type', AuditOperationTypes.COMMENT); - const audits = await query; return audits?.map((a) => new Audit(a)); @@ -199,27 +182,6 @@ export default class Audit implements AuditType { )?.count; } - static async deleteRowComments(fk_model_id: string, ncMeta = Noco.ncMeta) { - return ncMeta.metaDelete(null, null, MetaTable.AUDIT, { - fk_model_id, - }); - } - - static async commentUpdate( - auditId: string, - audit: Partial, - ncMeta = Noco.ncMeta, - ) { - const updateObj = extractProps(audit, ['description']); - return await ncMeta.metaUpdate( - null, - null, - MetaTable.AUDIT, - updateObj, - auditId, - ); - } - static async sourceAuditList(sourceId: string, { limit = 25, offset = 0 }) { return await Noco.ncMeta.metaList2(null, null, MetaTable.AUDIT, { condition: { source_id: sourceId }, diff --git a/packages/nocodb/src/models/Comment.ts b/packages/nocodb/src/models/Comment.ts new file mode 100644 index 0000000000..784a689399 --- /dev/null +++ b/packages/nocodb/src/models/Comment.ts @@ -0,0 +1,154 @@ +import type { CommentType } from 'nocodb-sdk'; +import Noco from '~/Noco'; +import { MetaTable } from '~/utils/globals'; +import { prepareForDb } from '~/utils/modelUtils'; +import { extractProps } from '~/helpers/extractProps'; +import Model from '~/models/Model'; + +export default class Comment implements CommentType { + id?: string; + fk_model_id?: string; + row_id?: string; + comment?: string; + parent_comment_id?: string; + source_id?: string; + base_id?: string; + created_by?: string; + resolved_by?: string; + created_by_email?: string; + resolved_by_email?: string; + is_deleted?: boolean; + + constructor(comment: Partial) { + Object.assign(this, comment); + } + + public static async get(commentId: string, ncMeta = Noco.ncMeta) { + const comment = await ncMeta.metaGet2( + null, + null, + MetaTable.COMMENTS, + commentId, + ); + + return comment && new Comment(comment); + } + + public static async list( + { + row_id, + fk_model_id, + }: { + row_id: string; + fk_model_id: string; + }, + ncMeta = Noco.ncMeta, + ) { + const commentList = await ncMeta + .knex(MetaTable.COMMENTS) + .select(`${MetaTable.COMMENTS}.*`) + .where('row_id', row_id) + .where('fk_model_id', fk_model_id) + .where(function () { + this.whereNull('is_deleted').orWhere('is_deleted', '!=', true); + }) + .orderBy('created_at', 'asc'); + + return commentList.map((comment) => new Comment(comment)); + } + + public static async insert(comment: Partial, ncMeta = Noco.ncMeta) { + const insertObj = extractProps(comment, [ + 'id', + 'fk_model_id', + 'row_id', + 'comment', + 'parent_comment_id', + 'source_id', + 'base_id', + 'fk_model_id', + 'created_by', + 'created_by_email', + ]); + + if ((!insertObj.base_id || !insertObj.source_id) && insertObj.fk_model_id) { + const model = await Model.getByIdOrName( + { id: insertObj.fk_model_id }, + ncMeta, + ); + + insertObj.base_id = model.base_id; + insertObj.source_id = model.source_id; + } + + const res = await ncMeta.metaInsert2( + null, + null, + MetaTable.COMMENTS, + prepareForDb(insertObj), + ); + + return res; + } + public static async update( + commentId: string, + comment: Partial, + ncMeta = Noco.ncMeta, + ) { + const updateObj = extractProps(comment, ['comment', 'resolved_by']); + + await ncMeta.metaUpdate( + null, + null, + MetaTable.COMMENTS, + prepareForDb(updateObj), + commentId, + ); + + return Comment.get(commentId, ncMeta); + } + + static async delete(commentId: string, ncMeta = Noco.ncMeta) { + await ncMeta.metaUpdate( + null, + null, + MetaTable.COMMENTS, + { is_deleted: true }, + commentId, + ); + + return true; + } + + static async deleteRowComments(fk_model_id: string, ncMeta = Noco.ncMeta) { + return ncMeta.metaUpdate( + null, + null, + MetaTable.COMMENTS, + { + is_deleted: true, + }, + { + fk_model_id, + }, + ); + } + + public static async commentsCount(args: { + ids: string[]; + fk_model_id: string; + }) { + const audits = await Noco.ncMeta + .knex(MetaTable.COMMENTS) + .count('id', { as: 'count' }) + .select('row_id') + .whereIn('row_id', args.ids) + .where('fk_model_id', args.fk_model_id) + .where(function () { + this.whereNull('is_deleted').orWhere('is_deleted', '!=', true); + }) + .groupBy('row_id'); + + return audits?.map((a) => new Comment(a)); + } +} diff --git a/packages/nocodb/src/models/Model.ts b/packages/nocodb/src/models/Model.ts index 8f62c1b173..5454f4ca8e 100644 --- a/packages/nocodb/src/models/Model.ts +++ b/packages/nocodb/src/models/Model.ts @@ -6,13 +6,12 @@ import { ViewTypes, } from 'nocodb-sdk'; import dayjs from 'dayjs'; - import type { BoolType, TableReqType, TableType } from 'nocodb-sdk'; import type { XKnex } from '~/db/CustomKnex'; import type { LinksColumn, LinkToAnotherRecordColumn } from '~/models/index'; import Hook from '~/models/Hook'; -import Audit from '~/models/Audit'; import View from '~/models/View'; +import Comment from '~/models/Comment'; import Column from '~/models/Column'; import { extractProps } from '~/helpers/extractProps'; import { sanitize } from '~/helpers/sqlSanitize'; @@ -433,7 +432,7 @@ export default class Model implements TableType { } async delete(ncMeta = Noco.ncMeta, force = false): Promise { - await Audit.deleteRowComments(this.id, ncMeta); + await Comment.deleteRowComments(this.id, ncMeta); for (const view of await this.getViews(true, ncMeta)) { await view.delete(ncMeta); diff --git a/packages/nocodb/src/models/Notification.ts b/packages/nocodb/src/models/Notification.ts index 02d299322a..636f96ba62 100644 --- a/packages/nocodb/src/models/Notification.ts +++ b/packages/nocodb/src/models/Notification.ts @@ -66,6 +66,9 @@ export default class Notification { condition, limit, offset, + orderBy: { + created_at: 'desc', + }, }, ); diff --git a/packages/nocodb/src/models/index.ts b/packages/nocodb/src/models/index.ts index 4d00fde3ad..ae0c876191 100644 --- a/packages/nocodb/src/models/index.ts +++ b/packages/nocodb/src/models/index.ts @@ -42,3 +42,4 @@ export { default as Notification } from './Notification'; export { default as PresignedUrl } from './PresignedUrl'; export { default as UserRefreshToken } from './UserRefreshToken'; export { default as Extension } from './Extension'; +export { default as Comment } from './Comment'; diff --git a/packages/nocodb/src/modules/noco.module.ts b/packages/nocodb/src/modules/noco.module.ts index 080e88e49d..b88e936809 100644 --- a/packages/nocodb/src/modules/noco.module.ts +++ b/packages/nocodb/src/modules/noco.module.ts @@ -33,6 +33,7 @@ import { SourcesController } from '~/controllers/sources.controller'; import { CachesController } from '~/controllers/caches.controller'; import { CalendarsController } from '~/controllers/calendars.controller'; import { ColumnsController } from '~/controllers/columns.controller'; +import { CommentsController } from '~/controllers/comments.controller'; import { FiltersController } from '~/controllers/filters.controller'; import { FormColumnsController } from '~/controllers/form-columns.controller'; import { FormsController } from '~/controllers/forms.controller'; @@ -64,6 +65,7 @@ import { SourcesService } from '~/services/sources.service'; import { CachesService } from '~/services/caches.service'; import { CalendarsService } from '~/services/calendars.service'; import { ColumnsService } from '~/services/columns.service'; +import { CommentsService } from '~/services/comments.service'; import { FiltersService } from '~/services/filters.service'; import { FormColumnsService } from '~/services/form-columns.service'; import { FormsService } from '~/services/forms.service'; @@ -148,6 +150,7 @@ export const nocoModuleMetadata = { CachesController, CalendarsController, ColumnsController, + CommentsController, FiltersController, FormColumnsController, FormsController, @@ -214,6 +217,7 @@ export const nocoModuleMetadata = { CachesService, CalendarsService, ColumnsService, + CommentsService, FiltersService, FormColumnsService, FormsService, diff --git a/packages/nocodb/src/schema/swagger-v2.json b/packages/nocodb/src/schema/swagger-v2.json index 93f24a2b05..2385784e61 100644 --- a/packages/nocodb/src/schema/swagger-v2.json +++ b/packages/nocodb/src/schema/swagger-v2.json @@ -8434,15 +8434,15 @@ "parameters": [] } }, - "/api/v2/meta/audits/comments": { + "/api/v2/meta/audits": { "parameters": [ { "$ref": "#/components/parameters/xc-auth" } ], "get": { - "summary": "List Comments in Audit", - "operationId": "utils-comment-list", + "summary": "List Audits", + "operationId": "utils-audit-list", "responses": { "200": { "description": "OK", @@ -8517,7 +8517,7 @@ "$ref": "#/components/responses/BadRequest" } }, - "description": "List all comments", + "description": "List all audits", "parameters": [ { "schema": { @@ -8539,14 +8539,113 @@ "required": true, "description": "Foreign Key to Model" }, + { + "$ref": "#/components/parameters/xc-auth" + } + ], + "tags": [ + "Utils" + ] + } + }, + "/api/v2/meta/comments": { + "parameters": [ + { + "$ref": "#/components/parameters/xc-auth" + } + ], + "get": { + "summary": "List Comments", + "operationId": "utils-comment-list", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "list": { + "x-stoplight": { + "id": "5zto1xohsngbu" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/Comment", + "x-stoplight": { + "id": "d22zkup0c0l80" + } + } + } + }, + "required": [ + "list" + ] + }, + "examples": { + "Example 1": { + "value": { + "list": [ + { + "id": "adt_3sii7erfwrlegb", + "source_id": null, + "base_id": "p_63b4q0qengen1x", + "fk_model_id": "md_5mipbdg6ketmv8", + "row_id": "1", + "created_by": "", + "resolved_by": "", + "parent_comment_id": null, + "status": null, + "comment": "bar", + "created_at": "2023-03-13T09:39:14.225Z", + "updated_at": "2023-03-13T09:39:14.225Z" + }, + { + "id": "adt_3sii7erfwrlegb", + "source_id": null, + "base_id": "p_63b4q0qengen1x", + "fk_model_id": "md_5mipbdg6ketmv8", + "row_id": "1", + "created_by": "", + "resolved_by": "", + "parent_comment_id": null, + "status": null, + "comment": "bar", + "created_at": "2023-03-13T09:39:14.225Z", + "updated_at": "2023-03-13T09:39:14.225Z" + } + ] + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + }, + "description": "List all comments", + "parameters": [ { "schema": { - "type": "boolean", - "example": true + "type": "string", + "example": "10" }, "in": "query", - "name": "comments_only", - "description": "Is showing comments only?" + "name": "row_id", + "required": true, + "description": "Row ID" + }, + { + "schema": { + "$ref": "#/components/schemas/Id", + "example": "md_c6csq89tl37jm5" + }, + "in": "query", + "name": "fk_model_id", + "required": true, + "description": "Foreign Key to Model" }, { "$ref": "#/components/parameters/xc-auth" @@ -8565,18 +8664,23 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Audit" + "$ref": "#/components/schemas/Comment" }, "examples": { "Example 1": { "value": { - "user": "w@nocodb.com", - "row_id": "1", - "fk_model_id": "md_5mipbdg6ketmv8", - "op_type": "COMMENT", - "description": "qq", + "id": "adt_3sii7erfwrlegb", + "source_id": null, "base_id": "p_63b4q0qengen1x", - "id": "adt_vx58jpp0loo8qy" + "fk_model_id": "md_5mipbdg6ketmv8", + "row_id": "1", + "created_by": "", + "resolved_by": "", + "parent_comment_id": null, + "status": null, + "comment": "bar", + "created_at": "2023-03-13T09:39:14.225Z", + "updated_at": "2023-03-13T09:39:14.225Z" } } } @@ -8596,7 +8700,7 @@ "examples": { "Example 1": { "value": { - "description": "This is the comment for the row", + "comment": "This is the comment for the row", "fk_model_id": "md_ehn5izr99m7d45", "row_id": "3" } @@ -8616,17 +8720,17 @@ ] } }, - "/api/v2/meta/audits/{auditId}/comment": { + "/api/v2/meta/comment/{commentId}": { "parameters": [ { "schema": { "type": "string", "example": "adt_zlskd6rlf3liay" }, - "name": "auditId", + "name": "commentId", "in": "path", "required": true, - "description": "Audit ID" + "description": "Comment ID" }, { "name": "xc-auth", @@ -8639,7 +8743,7 @@ } ], "patch": { - "summary": "Update Comment in Audit", + "summary": "Update Comment", "operationId": "utils-comment-update", "responses": { "200": { @@ -8662,7 +8766,7 @@ "tags": [ "Utils" ], - "description": "Update comment in Audit", + "description": "Update comment", "requestBody": { "content": { "application/json": { @@ -8672,16 +8776,46 @@ "examples": { "Example 1": { "value": { - "description": "This is the comment for the row" + "comment": "This is the comment for the row" } } } } } } + }, + "delete": { + "summary": "Delete Comment", + "operationId": "utils-comment-delete", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "number", + "example": 1 + }, + "examples": { + "Example 1": { + "value": 1 + } + } + } + } + } + }, + "tags": [ + "Utils" + ], + "description": "Delete comment", + "requestBody": { + "content": { + } + } } }, - "/api/v2/meta/audits/comments/count": { + "/api/v2/meta/comments/count": { "parameters": [ { "$ref": "#/components/parameters/xc-auth" @@ -11750,6 +11884,66 @@ ], "title": "Attachment Response Model" }, + "Comment": { + "description": "Model for Comment", + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/Id", + "description": "Unique ID" + }, + "row_id": { + "type": "string", + "example": "rec0Adp9PMG9o7uJy", + "description": "Row ID" + }, + "comment": { + "type": "string", + "example": "This is a comment", + "description": "Comment" + }, + "created_by": { + "$ref": "#/components/schemas/Id", + "example": "usr0Adp9PMG9o7uJy", + "description": "Created By" + }, + "resolved_by": { + "$ref": "#/components/schemas/Id", + "example": "usr0Adp9PMG9o7uJy", + "description": "Resolved By" + }, + "parent_comment_id": { + "$ref": "#/components/schemas/Id", + "example": "cmt043cx4r30343ff", + "description": "Parent Comment ID" + }, + "source_id": { + "$ref": "#/components/schemas/Id", + "example": "src0Adp9PMG9o7uJy", + "description": "Source ID" + }, + "base_id": { + "$ref": "#/components/schemas/Id", + "example": "bas0Adp9PMG9o7uJy", + "description": "Base ID" + }, + "fk_model_id": { + "$ref": "#/components/schemas/Id", + "example": "mod0Adp9PMG9o7uJy", + "description": "Model ID" + }, + "created_at": { + "type": "string", + "example": "2020-05-20T12:00:00.000000Z", + "description": "Created At" + }, + "updated_at": { + "type": "string", + "example": "2020-05-20T12:00:00.000000Z", + "description": "Updated At" + } + } + }, "Audit": { "description": "Model for Audit", "examples": [ @@ -12692,7 +12886,7 @@ "description": "Model for Comment Request", "examples": [ { - "description": "This is the comment for the row", + "comment": "This is the comment for the row", "fk_model_id": "md_ehn5izr99m7d45", "row_id": "3" } @@ -12700,7 +12894,7 @@ "title": "Comment Request Model", "type": "object", "properties": { - "description": { + "comment": { "type": "string", "description": "Description for the target row", "example": "This is the comment for the row" @@ -12731,16 +12925,22 @@ }, "examples": [ { - "description": "This is the comment for the row" + "comment": "This is the comment for the row", + "fk_model_id": "md_ehn5izr99m7d45" } ], "title": "Comment Update Request Model", "type": "object", "properties": { - "description": { + "comment": { "type": "string", "description": "Description for the target row", "example": "This is the comment for the row" + }, + "fk_model_id": { + "type": "string", + "description": "Foreign Key to Model", + "example": "md_ehn5izr99m7d45" } } }, @@ -14976,7 +15176,7 @@ "x-stoplight": { "id": "uqq8xmyz97t1u" }, - "description": "Baes ID\n" + "description": "Base ID\n" }, "base_id": { "$ref": "#/components/schemas/Id", diff --git a/packages/nocodb/src/schema/swagger.json b/packages/nocodb/src/schema/swagger.json index 7203abbf42..3a61d497c7 100644 --- a/packages/nocodb/src/schema/swagger.json +++ b/packages/nocodb/src/schema/swagger.json @@ -13529,15 +13529,15 @@ "parameters": [] } }, - "/api/v1/db/meta/audits/comments": { + "/api/v1/db/meta/audits": { "parameters": [ { "$ref": "#/components/parameters/xc-auth" } ], "get": { - "summary": "List Comments in Audit", - "operationId": "utils-comment-list", + "summary": "List Audits", + "operationId": "utils-audit-list", "responses": { "200": { "description": "OK", @@ -13612,7 +13612,7 @@ "$ref": "#/components/responses/BadRequest" } }, - "description": "List all comments", + "description": "List all audits", "parameters": [ { "schema": { @@ -13634,14 +13634,113 @@ "required": true, "description": "Foreign Key to Model" }, + { + "$ref": "#/components/parameters/xc-auth" + } + ], + "tags": [ + "Utils" + ] + } + }, + "/api/v1/db/meta/comments": { + "parameters": [ + { + "$ref": "#/components/parameters/xc-auth" + } + ], + "get": { + "summary": "List Comments", + "operationId": "utils-comment-list", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "list": { + "x-stoplight": { + "id": "5zto1xohsngbu" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/Comment", + "x-stoplight": { + "id": "d22zkup0c0l80" + } + } + } + }, + "required": [ + "list" + ] + }, + "examples": { + "Example 1": { + "value": { + "list": [ + { + "id": "adt_3sii7erfwrlegb", + "source_id": null, + "base_id": "p_63b4q0qengen1x", + "fk_model_id": "md_5mipbdg6ketmv8", + "row_id": "1", + "created_by": "", + "resolved_by": "", + "parent_comment_id": null, + "status": null, + "comment": "bar", + "created_at": "2023-03-13T09:39:14.225Z", + "updated_at": "2023-03-13T09:39:14.225Z" + }, + { + "id": "adt_3sii7erfwrlegb", + "source_id": null, + "base_id": "p_63b4q0qengen1x", + "fk_model_id": "md_5mipbdg6ketmv8", + "row_id": "1", + "created_by": "", + "resolved_by": "", + "parent_comment_id": null, + "status": null, + "comment": "bar", + "created_at": "2023-03-13T09:39:14.225Z", + "updated_at": "2023-03-13T09:39:14.225Z" + } + ] + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + }, + "description": "List all comments", + "parameters": [ { "schema": { - "type": "boolean", - "example": true + "type": "string", + "example": "10" }, "in": "query", - "name": "comments_only", - "description": "Is showing comments only?" + "name": "row_id", + "required": true, + "description": "Row ID" + }, + { + "schema": { + "$ref": "#/components/schemas/Id", + "example": "md_c6csq89tl37jm5" + }, + "in": "query", + "name": "fk_model_id", + "required": true, + "description": "Foreign Key to Model" }, { "$ref": "#/components/parameters/xc-auth" @@ -13660,18 +13759,23 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Audit" + "$ref": "#/components/schemas/Comment" }, "examples": { "Example 1": { "value": { - "user": "w@nocodb.com", - "row_id": "1", - "fk_model_id": "md_5mipbdg6ketmv8", - "op_type": "COMMENT", - "description": "qq", + "id": "adt_3sii7erfwrlegb", + "source_id": null, "base_id": "p_63b4q0qengen1x", - "id": "adt_vx58jpp0loo8qy" + "fk_model_id": "md_5mipbdg6ketmv8", + "row_id": "1", + "created_by": "", + "resolved_by": "", + "parent_comment_id": null, + "status": null, + "comment": "bar", + "created_at": "2023-03-13T09:39:14.225Z", + "updated_at": "2023-03-13T09:39:14.225Z" } } } @@ -13703,7 +13807,7 @@ "tags": [ "Utils" ], - "description": "Create a new comment in a row. Logged in Audit.", + "description": "Create a new comment in a row.", "parameters": [ { "$ref": "#/components/parameters/xc-auth" @@ -13711,17 +13815,17 @@ ] } }, - "/api/v1/db/meta/audits/{auditId}/comment": { + "/api/v1/db/meta/comment/{commentId}/": { "parameters": [ { "schema": { "type": "string", "example": "adt_zlskd6rlf3liay" }, - "name": "auditId", + "name": "commentId", "in": "path", "required": true, - "description": "Audit ID" + "description": "Comment ID" }, { "name": "xc-auth", @@ -13734,7 +13838,7 @@ } ], "patch": { - "summary": "Update Comment in Audit", + "summary": "Update Comment", "operationId": "utils-comment-update", "responses": { "200": { @@ -13757,7 +13861,7 @@ "tags": [ "Utils" ], - "description": "Update comment in Audit", + "description": "Update comment", "requestBody": { "content": { "application/json": { @@ -13767,16 +13871,46 @@ "examples": { "Example 1": { "value": { - "description": "This is the comment for the row" + "comment": "This is the comment for the row" + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete Comment", + "operationId": "utils-comment-delete", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "number", + "example": 1 + }, + "examples": { + "Example 1": { + "value": 1 } } } } } + }, + "tags": [ + "Utils" + ], + "description": "Delete comment", + "requestBody": { + "content": { + } } } }, - "/api/v1/db/meta/audits/comments/count": { + "/api/v1/db/meta/comments/count": { "parameters": [ { "$ref": "#/components/parameters/xc-auth" @@ -18782,7 +18916,7 @@ "description": "Model for Comment Request", "examples": [ { - "description": "This is the comment for the row", + "comment": "This is the comment for the row", "fk_model_id": "md_ehn5izr99m7d45", "row_id": "3" } @@ -18790,7 +18924,7 @@ "title": "Comment Request Model", "type": "object", "properties": { - "description": { + "comment": { "type": "string", "description": "Description for the target row", "example": "This is the comment for the row" @@ -18821,17 +18955,23 @@ }, "examples": [ { - "description": "This is the comment for the row" + "comment": "This is the comment for the row", + "fk_model_id": "md_ehn5izr99m7d45" } ], "title": "Comment Update Request Model", "type": "object", "properties": { - "description": { + "comment": { "type": "string", "description": "Description for the target row", "example": "This is the comment for the row" - } + }, + "fk_model_id": { + "type": "string", + "description": "Foreign Key to Model", + "example": "md_ehn5izr99m7d45" + } } }, "Filter": { @@ -25606,6 +25746,179 @@ "id": "psbv6c6y9qvbu" } }, + "Comment": { + "description": "Model for Comment", + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/Id", + "description": "Unique ID" + }, + "row_id": { + "type": "string", + "example": "rec0Adp9PMG9o7uJy", + "description": "Row ID" + }, + "comment": { + "type": "string", + "example": "This is a comment", + "description": "Comment" + }, + "created_by": { + "$ref": "#/components/schemas/Id", + "example": "usr0Adp9PMG9o7uJy", + "description": "Created By User ID" + }, + "created_by_email": { + "type": "string", + "example": "xxx@nocodb.com", + "description": "Created By User Email" + }, + "resolved_by": { + "$ref": "#/components/schemas/Id", + "example": "usr0Adp9PMG9o7uJy", + "description": "Resolved By User ID" + }, + "resolved_by_email": { + "type": "string", + "example": "xxx@nocodb.com", + "description": "Resolved By User Email" + }, + "parent_comment_id": { + "$ref": "#/components/schemas/Id", + "example": "cmt043cx4r30343ff", + "description": "Parent Comment ID" + }, + "source_id": { + "$ref": "#/components/schemas/Id", + "example": "src0Adp9PMG9o7uJy", + "description": "Source ID" + }, + "base_id": { + "$ref": "#/components/schemas/Id", + "example": "bas0Adp9PMG9o7uJy", + "description": "Base ID" + }, + "fk_model_id": { + "$ref": "#/components/schemas/Id", + "example": "mod0Adp9PMG9o7uJy", + "description": "Model ID" + }, + "created_at": { + "type": "string", + "example": "2020-05-20T12:00:00.000000Z", + "description": "Created At" + }, + "updated_at": { + "type": "string", + "example": "2020-05-20T12:00:00.000000Z", + "description": "Updated At" + }, + "is_deleted": { + "type": "boolean", + "description": "Whether the comment has been deleted by the user or not" + } + } + }, + "UserCommentNotificationPreference": { + "description" : "Model for User Comment Notification Preference", + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/Id", + "description": "Unique ID" + }, + "row_id": { + "type": "string", + "description": "User ID" + }, + "user_id": { + "$ref": "#/components/schemas/Id", + "description": "User ID" + }, + "source_id": { + "$ref": "#/components/schemas/Id", + "example": "src0Adp9PMG9o7uJy", + "description": "Source ID" + }, + "base_id": { + "$ref": "#/components/schemas/Id", + "example": "bas0Adp9PMG9o7uJy", + "description": "Base ID" + }, + "fk_model_id": { + "$ref": "#/components/schemas/Id", + "example": "mod0Adp9PMG9o7uJy", + "description": "Model ID" + }, + "preference": { + "type": "string", + "enum": [ + "ALL_COMMENTS", + "ONLY_MENTIONS" + ], + "description": "Is Read" + }, + "created_at": { + "type": "string", + "description": "Created At" + }, + "updated_at": { + "type": "string", + "description": "Updated At" + } + } + }, + "CommentReactions": { + "description": "Model for Comment Reactions", + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/Id", + "description": "Unique ID" + }, + "row_id": { + "type": "string", + "description": "Row ID" + }, + "comment_id": { + "$ref": "#/components/schemas/Id", + "description": "Comment ID" + }, + "reaction": { + "type": "string", + "description": "Reaction" + }, + "user_id": { + "$ref": "#/components/schemas/Id", + "description": "User ID" + }, + "source_id": { + "$ref": "#/components/schemas/Id", + "example": "src0Adp9PMG9o7uJy", + "description": "Source ID" + }, + "base_id": { + "$ref": "#/components/schemas/Id", + "example": "bas0Adp9PMG9o7uJy", + "description": "Base ID" + }, + "fk_model_id": { + "$ref": "#/components/schemas/Id", + "example": "mod0Adp9PMG9o7uJy", + "description": "Model ID" + }, + "created_at": { + "type": "string", + "description": "Created At" + }, + "updated_at": { + "type": "string", + "description": "Updated At" + } + } + + }, "Extension": { "type": "object", "properties": { diff --git a/packages/nocodb/src/services/app-hooks/app-hooks.service.ts b/packages/nocodb/src/services/app-hooks/app-hooks.service.ts index 5aaab7f54a..63c098f866 100644 --- a/packages/nocodb/src/services/app-hooks/app-hooks.service.ts +++ b/packages/nocodb/src/services/app-hooks/app-hooks.service.ts @@ -1,42 +1,42 @@ import { Inject, Injectable } from '@nestjs/common'; + +import type { AppEvents } from 'nocodb-sdk'; import type { ApiCreatedEvent, ApiTokenCreateEvent, ApiTokenDeleteEvent, AttachmentEvent, BaseEvent, + ColumnEvent, + FilterEvent, FormColumnEvent, GridColumnEvent, MetaDiffEvent, OrgUserInviteEvent, PluginEvent, PluginTestEvent, + ProjectCreateEvent, + ProjectDeleteEvent, + ProjectInviteEvent, + ProjectUpdateEvent, ProjectUserResendInviteEvent, ProjectUserUpdateEvent, RelationEvent, + RowCommentEvent, SharedBaseEvent, + SortEvent, SyncSourceEvent, + TableEvent, UIAclEvent, UserEmailVerificationEvent, UserPasswordChangeEvent, UserPasswordForgotEvent, UserPasswordResetEvent, - ViewColumnEvent, - WebhookEvent, -} from './interfaces'; -import type { AppEvents } from 'nocodb-sdk'; -import type { - ColumnEvent, - FilterEvent, - ProjectCreateEvent, - ProjectDeleteEvent, - ProjectInviteEvent, - ProjectUpdateEvent, - SortEvent, - TableEvent, UserSigninEvent, UserSignupEvent, + ViewColumnEvent, ViewEvent, + WebhookEvent, WelcomeEvent, } from '~/services/app-hooks/interfaces'; import { IEventEmitter } from '~/modules/event-emitter/event-emitter.interface'; @@ -52,6 +52,14 @@ export class AppHooksService { @Inject('IEventEmitter') protected readonly eventEmitter: IEventEmitter, ) {} + on( + event: + | AppEvents.COMMENT_CREATE + | AppEvents.COMMENT_UPDATE + | AppEvents.COMMENT_DELETE, + listener: (data: RowCommentEvent) => void, + ); + on( event: AppEvents.PROJECT_INVITE, listener: (data: ProjectInviteEvent) => void, @@ -164,6 +172,13 @@ export class AppHooksService { | AppEvents.SHARED_VIEW_CREATE, data: ViewEvent, ): void; + emit( + event: + | AppEvents.COMMENT_CREATE + | AppEvents.COMMENT_UPDATE + | AppEvents.COMMENT_DELETE, + data: RowCommentEvent, + ): void; emit( event: | AppEvents.FILTER_UPDATE diff --git a/packages/nocodb/src/services/app-hooks/interfaces.ts b/packages/nocodb/src/services/app-hooks/interfaces.ts index 5fc492fe57..32d9083d8f 100644 --- a/packages/nocodb/src/services/app-hooks/interfaces.ts +++ b/packages/nocodb/src/services/app-hooks/interfaces.ts @@ -1,17 +1,15 @@ import type { SyncSource } from '~/models'; import type { ApiTokenReqType, - PluginTestReqType, - PluginType, - SourceType, -} from 'nocodb-sdk'; -import type { BaseType, ColumnType, FilterType, HookType, + PluginTestReqType, + PluginType, ProjectUserReqType, SortType, + SourceType, TableType, UserType, ViewType, @@ -30,6 +28,15 @@ export interface ProjectInviteEvent extends NcBaseEvent { ip?: string; } +export interface RowCommentEvent extends NcBaseEvent { + base: BaseType; + user: UserType; + model: TableType; + rowId: string; + comment: string; + ip?: string; +} + export interface ProjectUserUpdateEvent extends NcBaseEvent { base: BaseType; user: UserType; @@ -213,4 +220,5 @@ export type AppEventPayload = | ViewEvent | FilterEvent | SortEvent + | RowCommentEvent | ColumnEvent; diff --git a/packages/nocodb/src/services/audits.service.ts b/packages/nocodb/src/services/audits.service.ts index 046cb92a12..3f39cbbb09 100644 --- a/packages/nocodb/src/services/audits.service.ts +++ b/packages/nocodb/src/services/audits.service.ts @@ -1,28 +1,19 @@ import { Injectable } from '@nestjs/common'; import DOMPurify from 'isomorphic-dompurify'; import { AuditOperationSubTypes, AuditOperationTypes } from 'nocodb-sdk'; -import type { AuditRowUpdateReqType, CommentUpdateReqType } from 'nocodb-sdk'; +import type { AuditRowUpdateReqType } from 'nocodb-sdk'; import { AppHooksListenerService } from '~/services/app-hooks-listener.service'; -import { NcError } from '~/helpers/catchError'; import { validatePayload } from '~/helpers'; import { Audit, Model } from '~/models'; +import { AppHooksService } from '~/services/app-hooks/app-hooks.service'; @Injectable() export class AuditsService { constructor( protected readonly appHooksListenerService: AppHooksListenerService, + protected readonly appHooksService: AppHooksService, ) {} - async commentRow(param: { body: AuditRowUpdateReqType; user: any }) { - validatePayload('swagger.json#/components/schemas/CommentReq', param.body); - - return await Audit.insert({ - ...param.body, - user: param.user?.email, - op_type: AuditOperationTypes.COMMENT, - }); - } - async auditRowUpdate(param: { rowId: string; body: AuditRowUpdateReqType }) { validatePayload( 'swagger.json#/components/schemas/AuditRowUpdateReq', @@ -63,8 +54,13 @@ export class AuditsService { // }) } - async commentList(param: { query: any }) { - return await Audit.commentsList(param.query); + async auditOnlyList(param: { + query: { + row_id: string; + fk_model_id: string; + }; + }) { + return await Audit.auditList(param.query); } async auditList(param: { query: any; baseId: string }) { @@ -75,34 +71,6 @@ export class AuditsService { return await Audit.baseAuditCount(param.baseId, param.query?.sourceId); } - async commentsCount(param: { fk_model_id: string; ids: string[] }) { - return await Audit.commentsCount({ - fk_model_id: param.fk_model_id as string, - ids: param.ids as string[], - }); - } - - async commentUpdate(param: { - auditId: string; - userEmail: string; - body: CommentUpdateReqType; - }) { - validatePayload( - 'swagger.json#/components/schemas/CommentUpdateReq', - param.body, - ); - - const log = await Audit.get(param.auditId); - if (log.op_type !== AuditOperationTypes.COMMENT) { - NcError.forbidden('Only comments can be updated'); - } - - if (log.user !== param.userEmail) { - NcError.unauthorized('Unauthorized access'); - } - return await Audit.commentUpdate(param.auditId, param.body); - } - async baseAuditList(param: { query: any; sourceId: any }) { return await Audit.baseAuditList(param.sourceId, param.query); } diff --git a/packages/nocodb/src/services/comments.service.ts b/packages/nocodb/src/services/comments.service.ts new file mode 100644 index 0000000000..4f4843afd6 --- /dev/null +++ b/packages/nocodb/src/services/comments.service.ts @@ -0,0 +1,118 @@ +import { Injectable } from '@nestjs/common'; +import { AppEvents } from 'nocodb-sdk'; +import { Base, Model } from '../models'; +import type { + CommentReqType, + CommentUpdateReqType, + UserType, +} from 'nocodb-sdk'; +import type { NcRequest } from '~/interface/config'; +import { NcError } from '~/helpers/catchError'; +import { validatePayload } from '~/helpers'; +import { AppHooksService } from '~/services/app-hooks/app-hooks.service'; +import Comment from '~/models/Comment'; + +@Injectable() +export class CommentsService { + constructor(protected readonly appHooksService: AppHooksService) {} + + async commentRow(param: { + body: CommentReqType; + user: UserType; + req: NcRequest; + }) { + validatePayload('swagger.json#/components/schemas/CommentReq', param.body); + + const res = await Comment.insert({ + ...param.body, + created_by: param.user?.id, + created_by_email: param.user?.email, + }); + + const model = await Model.getByIdOrName({ id: param.body.fk_model_id }); + + this.appHooksService.emit(AppEvents.COMMENT_CREATE, { + base: await Base.getByTitleOrId(model.base_id), + model: model, + user: param.user, + comment: param.body.comment, + rowId: param.body.row_id, + req: param.req, + }); + + return res; + } + + async commentDelete(param: { commentId: string; user: UserType }) { + const comment = await Comment.get(param.commentId); + + if (comment.created_by !== param.user.id || comment.is_deleted) { + NcError.unauthorized('Unauthorized access'); + } + + const res = await Comment.delete(param.commentId); + + const model = await Model.getByIdOrName({ id: comment.fk_model_id }); + + this.appHooksService.emit(AppEvents.COMMENT_DELETE, { + base: await Base.getByTitleOrId(model.base_id), + model: model, + user: param.user, + comment: comment.comment, + rowId: comment.row_id, + req: {}, + }); + return res; + } + + async commentList(param: { + query: { + row_id: string; + fk_model_id: string; + }; + }) { + return await Comment.list(param.query); + } + + async commentsCount(param: { fk_model_id: string; ids: string[] }) { + return await Comment.commentsCount({ + fk_model_id: param.fk_model_id as string, + ids: param.ids as string[], + }); + } + + async commentUpdate(param: { + commentId: string; + user: UserType; + body: CommentUpdateReqType; + req: NcRequest; + }) { + validatePayload( + 'swagger.json#/components/schemas/CommentUpdateReq', + param.body, + ); + + const comment = await Comment.get(param.commentId); + + if (comment.created_by !== param.user.id || comment.is_deleted) { + NcError.unauthorized('Unauthorized access'); + } + + const res = await Comment.update(param.commentId, { + comment: param.body.comment, + }); + + const model = await Model.getByIdOrName({ id: param.body.fk_model_id }); + + this.appHooksService.emit(AppEvents.COMMENT_UPDATE, { + base: await Base.getByTitleOrId(model.base_id), + model: model, + user: param.user, + comment: param.body.comment, + rowId: comment.row_id, + req: param.req, + }); + + return res; + } +} diff --git a/packages/nocodb/src/utils/acl.ts b/packages/nocodb/src/utils/acl.ts index 0a12e1ec8c..c16e5d031d 100644 --- a/packages/nocodb/src/utils/acl.ts +++ b/packages/nocodb/src/utils/acl.ts @@ -92,6 +92,7 @@ const permissionScopes = { 'swaggerJson', 'commentList', 'commentsCount', + 'commentDelete', 'commentUpdate', 'hideAllColumns', 'showAllColumns', @@ -210,6 +211,7 @@ const rolePermissions: commentsCount: true, commentRow: true, commentUpdate: true, + commentDelete: true, }, }, [ProjectRoles.EDITOR]: { diff --git a/packages/nocodb/src/utils/globals.ts b/packages/nocodb/src/utils/globals.ts index 6bdfbe9353..5b02edbfb8 100644 --- a/packages/nocodb/src/utils/globals.ts +++ b/packages/nocodb/src/utils/globals.ts @@ -48,6 +48,9 @@ export enum MetaTable { NOTIFICATION = 'notification', USER_REFRESH_TOKENS = 'nc_user_refresh_tokens', EXTENSIONS = 'nc_extensions', + COMMENTS = 'nc_comments', + USER_COMMENTS_NOTIFICATIONS_PREFERENCE = 'nc_user_comment_notifications_preference', + COMMENTS_REACTIONS = 'nc_comment_reactions', } export enum MetaTableOldV2 { diff --git a/packages/nocodb/src/utils/richTextHelper.ts b/packages/nocodb/src/utils/richTextHelper.ts new file mode 100644 index 0000000000..17490af436 --- /dev/null +++ b/packages/nocodb/src/utils/richTextHelper.ts @@ -0,0 +1,17 @@ +export const extractMentions = (richText: string) => { + const mentions: string[] = []; + + // The Mentions are stored as follows @(userId|email|display_name) in the rich text + // Extracts the userId from the content + + const regex = /@\(([^)]+)\)/g; + + let match: RegExpExecArray | null; + match = regex.exec(richText); + while (match !== null) { + const userId = match[1].split('|')[0]; // Extracts the userId part from the matched string + mentions.push(userId); + } + + return Array.from(new Set(mentions)); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85a8b625ba..11709198c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,29 +53,29 @@ importers: specifier: ^0.5.1 version: 0.5.1(vue@3.4.27) '@tiptap/extension-link': - specifier: 2.2.6 - version: 2.2.6(@tiptap/core@2.3.1)(@tiptap/pm@2.2.6) + specifier: ^2.4.0 + version: 2.4.0(@tiptap/core@2.3.1)(@tiptap/pm@2.4.0) '@tiptap/extension-placeholder': - specifier: ^2.2.6 - version: 2.2.6(@tiptap/core@2.3.1)(@tiptap/pm@2.2.6) + specifier: ^2.4.0 + version: 2.4.0(@tiptap/core@2.3.1)(@tiptap/pm@2.4.0) '@tiptap/extension-task-list': - specifier: 2.2.6 - version: 2.2.6(@tiptap/core@2.3.1) + specifier: 2.4.0 + version: 2.4.0(@tiptap/core@2.3.1) '@tiptap/extension-underline': - specifier: ^2.2.6 - version: 2.2.6(@tiptap/core@2.3.1) + specifier: ^2.4.0 + version: 2.4.0(@tiptap/core@2.3.1) '@tiptap/html': - specifier: 2.2.6 - version: 2.2.6(@tiptap/core@2.3.1)(@tiptap/pm@2.2.6) + specifier: 2.4.0 + version: 2.4.0(@tiptap/core@2.3.1)(@tiptap/pm@2.4.0) '@tiptap/pm': - specifier: ^2.2.6 - version: 2.2.6 + specifier: ^2.4.0 + version: 2.4.0 '@tiptap/starter-kit': - specifier: ^2.2.6 - version: 2.2.6(@tiptap/pm@2.2.6) + specifier: ^2.4.0 + version: 2.4.0(@tiptap/pm@2.4.0) '@tiptap/vue-3': - specifier: 2.2.6 - version: 2.2.6(@tiptap/core@2.3.1)(@tiptap/pm@2.2.6)(vue@3.4.27) + specifier: 2.4.0 + version: 2.4.0(@tiptap/core@2.3.1)(@tiptap/pm@2.4.0)(vue@3.4.27) '@vue-flow/additional-components': specifier: ^1.3.3 version: 1.3.3(@vue-flow/core@1.31.0)(vue@3.4.27) @@ -8861,248 +8861,248 @@ packages: resolution: {integrity: sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==} dev: false - /@tiptap/core@2.2.6(@tiptap/pm@2.2.6): - resolution: {integrity: sha512-v7S7RhQhTXQo9KSk2jM/jJlTd3clU2FsJA3Omjz7GbgYtPSy67qSiaTbH/tWq12GzDHbKymx+oQnKmyx+yPucA==} + /@tiptap/core@2.3.1(@tiptap/pm@2.4.0): + resolution: {integrity: sha512-ycpQlmczAOc05TgB5sc3RUTEEBXAVmS8MR9PqQzg96qidaRfVkgE+2w4k7t83PMHl2duC0MGqOCy96pLYwSpeg==} peerDependencies: '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/pm': 2.2.6 + '@tiptap/pm': 2.4.0 dev: false - /@tiptap/core@2.3.1(@tiptap/pm@2.2.6): - resolution: {integrity: sha512-ycpQlmczAOc05TgB5sc3RUTEEBXAVmS8MR9PqQzg96qidaRfVkgE+2w4k7t83PMHl2duC0MGqOCy96pLYwSpeg==} + /@tiptap/core@2.4.0(@tiptap/pm@2.4.0): + resolution: {integrity: sha512-YJSahk8pkxpCs8SflCZfTnJpE7IPyUWIylfgXM2DefjRQa5DZ+c6sNY0s/zbxKYFQ6AuHVX40r9pCfcqHChGxQ==} peerDependencies: '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/pm': 2.2.6 + '@tiptap/pm': 2.4.0 dev: false - /@tiptap/extension-blockquote@2.2.6(@tiptap/core@2.2.6): - resolution: {integrity: sha512-Qoq4Tl4wyEGfuBrMFth5hWP1SroJtgDYPnyzAZeLiGzF3Yxtu7FFqjGtD1/Bos9ftnFVCAj+nIXnuKsM1YUaGg==} + /@tiptap/extension-blockquote@2.4.0(@tiptap/core@2.4.0): + resolution: {integrity: sha512-nJJy4KsPgQqWTTDOWzFRdjCfG5+QExfZj44dulgDFNh+E66xhamnbM70PklllXJgEcge7xmT5oKM0gKls5XgFw==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) dev: false - /@tiptap/extension-bold@2.2.6(@tiptap/core@2.2.6): - resolution: {integrity: sha512-PI/jNH7rmi6hBvWy/z+3KUTYqeaDXBUjidM74gWP6OLV28HTJ5SkIPCriYe4u2j2Wc/nk3gPxs4/hPOAu/YiXA==} + /@tiptap/extension-bold@2.4.0(@tiptap/core@2.4.0): + resolution: {integrity: sha512-csnW6hMDEHoRfxcPRLSqeJn+j35Lgtt1YRiOwn7DlS66sAECGRuoGfCvQSPij0TCDp4VCR9if5Sf8EymhnQumQ==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) dev: false - /@tiptap/extension-bubble-menu@2.2.6(@tiptap/core@2.3.1)(@tiptap/pm@2.2.6): - resolution: {integrity: sha512-nRWxbgkInhdGUL+e6iISgALcWh8A1PxeVB66w7yYZHS/WoZO0DXdXYT/BWb/XmEJ8r6B4c9SDZRklCiXT8dSXw==} + /@tiptap/extension-bubble-menu@2.4.0(@tiptap/core@2.3.1)(@tiptap/pm@2.4.0): + resolution: {integrity: sha512-s99HmttUtpW3rScWq8rqk4+CGCwergNZbHLTkF6Rp6TSboMwfp+rwL5Q/JkcAG9KGLso1vGyXKbt1xHOvm8zMw==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.3.1(@tiptap/pm@2.2.6) - '@tiptap/pm': 2.2.6 + '@tiptap/core': 2.3.1(@tiptap/pm@2.4.0) + '@tiptap/pm': 2.4.0 tippy.js: 6.3.7 dev: false - /@tiptap/extension-bullet-list@2.2.6(@tiptap/core@2.2.6): - resolution: {integrity: sha512-bSrmYlWfj/bXXoBMVB+gCTlsficVVzWi1jcAjAn+qNAENkhampmlFIUG4DiKGYtn18ZoTbyLgQGDMCO3SBdeDQ==} + /@tiptap/extension-bullet-list@2.4.0(@tiptap/core@2.4.0): + resolution: {integrity: sha512-9S5DLIvFRBoExvmZ+/ErpTvs4Wf1yOEs8WXlKYUCcZssK7brTFj99XDwpHFA29HKDwma5q9UHhr2OB2o0JYAdw==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) dev: false - /@tiptap/extension-code-block@2.2.6(@tiptap/core@2.2.6)(@tiptap/pm@2.2.6): - resolution: {integrity: sha512-834gVybNyI4nY6NINqnOosFPa4WKylMQTraEY2KhUH2XU1mh0Ni7EgyK10dfZvOUj90OjaxZtXkyZrZ89RTxog==} + /@tiptap/extension-code-block@2.4.0(@tiptap/core@2.4.0)(@tiptap/pm@2.4.0): + resolution: {integrity: sha512-QWGdv1D56TBGbbJSj2cIiXGJEKguPiAl9ONzJ/Ql1ZksiQsYwx0YHriXX6TOC//T4VIf6NSClHEtwtxWBQ/Csg==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) - '@tiptap/pm': 2.2.6 + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) + '@tiptap/pm': 2.4.0 dev: false - /@tiptap/extension-code@2.2.6(@tiptap/core@2.2.6): - resolution: {integrity: sha512-UGsSFvVWrWWWQFU4atk+b/qeewTLadOZG/BHZXQDloyP5eJ1SkgUVy9nv3y2cT8QWRbvF6sxkV+SdFoWnvaG3Q==} + /@tiptap/extension-code@2.4.0(@tiptap/core@2.4.0): + resolution: {integrity: sha512-wjhBukuiyJMq4cTcK3RBTzUPV24k5n1eEPlpmzku6ThwwkMdwynnMGMAmSF3fErh3AOyOUPoTTjgMYN2d10SJA==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) dev: false - /@tiptap/extension-document@2.2.6(@tiptap/core@2.2.6): - resolution: {integrity: sha512-yT9m5Oo9U/xAypcylaLiDE8qmVd3SCZSc8s5lqyC1OW+psb1oC0d14+TgKetO2s8K2wAbW2DxYG3yoxWffGYsQ==} + /@tiptap/extension-document@2.4.0(@tiptap/core@2.4.0): + resolution: {integrity: sha512-3jRodQJZDGbXlRPERaloS+IERg/VwzpC1IO6YSJR9jVIsBO6xC29P3cKTQlg1XO7p6ZH/0ksK73VC5BzzTwoHg==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) dev: false - /@tiptap/extension-dropcursor@2.2.6(@tiptap/core@2.2.6)(@tiptap/pm@2.2.6): - resolution: {integrity: sha512-mCeIbbfe4rl8CuxVQvT7iYSKGVX/ls1LOwALwlHJz5Uw5l3VknAJdjEmHt6hNFdHu162JivL02Il0QYQ8BZwvA==} + /@tiptap/extension-dropcursor@2.4.0(@tiptap/core@2.4.0)(@tiptap/pm@2.4.0): + resolution: {integrity: sha512-c46HoG2PEEpSZv5rmS5UX/lJ6/kP1iVO0Ax+6JrNfLEIiDULUoi20NqdjolEa38La2VhWvs+o20OviiTOKEE9g==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) - '@tiptap/pm': 2.2.6 + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) + '@tiptap/pm': 2.4.0 dev: false - /@tiptap/extension-floating-menu@2.2.6(@tiptap/core@2.3.1)(@tiptap/pm@2.2.6): - resolution: {integrity: sha512-6ONKC6Dx8zCc5YffXpnQ9FxGRoUp5Jm9mOO3losgiDFhdJqaO7SCk1ziOiD7enoWqIc2shZh8ADnqttCfnFVFQ==} + /@tiptap/extension-floating-menu@2.4.0(@tiptap/core@2.3.1)(@tiptap/pm@2.4.0): + resolution: {integrity: sha512-vLb9v+htbHhXyty0oaXjT3VC8St4xuGSHWUB9GuAJAQ+NajIO6rBPbLUmm9qM0Eh2zico5mpSD1Qtn5FM6xYzg==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.3.1(@tiptap/pm@2.2.6) - '@tiptap/pm': 2.2.6 + '@tiptap/core': 2.3.1(@tiptap/pm@2.4.0) + '@tiptap/pm': 2.4.0 tippy.js: 6.3.7 dev: false - /@tiptap/extension-gapcursor@2.2.6(@tiptap/core@2.2.6)(@tiptap/pm@2.2.6): - resolution: {integrity: sha512-HDYu+FmL9V+khsiT5904Dy2qG6KrAvnXEjZk1+vVul0TabnQvl2rqHjTxmev3P1rOYTgePmaWXazxAWFIvbMBQ==} + /@tiptap/extension-gapcursor@2.4.0(@tiptap/core@2.4.0)(@tiptap/pm@2.4.0): + resolution: {integrity: sha512-F4y/0J2lseohkFUw9P2OpKhrJ6dHz69ZScABUvcHxjznJLd6+0Zt7014Lw5PA8/m2d/w0fX8LZQ88pZr4quZPQ==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) - '@tiptap/pm': 2.2.6 + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) + '@tiptap/pm': 2.4.0 dev: false - /@tiptap/extension-hard-break@2.2.6(@tiptap/core@2.2.6): - resolution: {integrity: sha512-gwavC76sn26XQLyDaDtf28KIcbhMYPP+C5pkbRvAhVSckQB3Ebz3GRttVbm/jp+Uifp3bmoQEzISGCONEdKQoQ==} + /@tiptap/extension-hard-break@2.4.0(@tiptap/core@2.4.0): + resolution: {integrity: sha512-3+Z6zxevtHza5IsDBZ4lZqvNR3Kvdqwxq/QKCKu9UhJN1DUjsg/l1Jn2NilSQ3NYkBYh2yJjT8CMo9pQIu776g==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) dev: false - /@tiptap/extension-heading@2.2.6(@tiptap/core@2.2.6): - resolution: {integrity: sha512-XOmY+uezm42xSO1ero2bRBMdQxWytpxLJS+2shK0QogZ3sDplnfWfP5KV9Z2juXjTdPgPWG0ZaHzIIaLquEcfA==} + /@tiptap/extension-heading@2.4.0(@tiptap/core@2.4.0): + resolution: {integrity: sha512-fYkyP/VMo7YHO76YVrUjd95Qeo0cubWn/Spavmwm1gLTHH/q7xMtbod2Z/F0wd6QHnc7+HGhO7XAjjKWDjldaw==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) dev: false - /@tiptap/extension-history@2.2.6(@tiptap/core@2.2.6)(@tiptap/pm@2.2.6): - resolution: {integrity: sha512-c2Aeozc+pHcpqghLjXRX/tGU/C+Gp6hApUWPXdhZw5Y/ARj6ZRwx2/ym2K8MOrJ36/W7gc7Xyxd9ZbG7m7pcjA==} + /@tiptap/extension-history@2.4.0(@tiptap/core@2.4.0)(@tiptap/pm@2.4.0): + resolution: {integrity: sha512-gr5qsKAXEVGr1Lyk1598F7drTaEtAxqZiuuSwTCzZzkiwgEQsWMWTWc9F8FlneCEaqe1aIYg6WKWlmYPaFwr0w==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) - '@tiptap/pm': 2.2.6 + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) + '@tiptap/pm': 2.4.0 dev: false - /@tiptap/extension-horizontal-rule@2.2.6(@tiptap/core@2.2.6)(@tiptap/pm@2.2.6): - resolution: {integrity: sha512-zyLU+Xlk8y3yBCblE8pFwqAP2Rju1csyAu45hi3NCJ6HDGQGdjy8oh+Xa8y2kTPxRNMZARxqB+vCiEoW3YZn2A==} + /@tiptap/extension-horizontal-rule@2.4.0(@tiptap/core@2.4.0)(@tiptap/pm@2.4.0): + resolution: {integrity: sha512-yDgxy+YxagcEsBbdWvbQiXYxsv3noS1VTuGwc9G7ZK9xPmBHJ5y0agOkB7HskwsZvJHoaSqNRsh7oZTkf0VR3g==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) - '@tiptap/pm': 2.2.6 + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) + '@tiptap/pm': 2.4.0 dev: false - /@tiptap/extension-italic@2.2.6(@tiptap/core@2.2.6): - resolution: {integrity: sha512-wB+Y6p2gbc1f2hKYeGNXRQ7P2xi3+JzD3PjSyC9Ss/yyujZhxSOtxBF0nzFXdI+7nmN0Qm4inwPDU/DVrIPb+A==} + /@tiptap/extension-italic@2.4.0(@tiptap/core@2.4.0): + resolution: {integrity: sha512-aaW/L9q+KNHHK+X73MPloHeIsT191n3VLd3xm6uUcFDnUNvzYJ/q65/1ZicdtCaOLvTutxdrEvhbkrVREX6a8g==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) dev: false - /@tiptap/extension-link@2.2.6(@tiptap/core@2.3.1)(@tiptap/pm@2.2.6): - resolution: {integrity: sha512-Jj0oXSfQ8gZlzzwd669B8sEKBkoK8xV31Lu55tRv9PKHSU6p9CUqBuxY8qR+cquCtO28f3u0cdl5o4HzeIUL5A==} + /@tiptap/extension-link@2.4.0(@tiptap/core@2.3.1)(@tiptap/pm@2.4.0): + resolution: {integrity: sha512-r3PjT0bjSKAorHAEBPA0icSMOlqALbxVlWU9vAc+Q3ndzt7ht0CTPNewzFF9kjzARABVt1cblXP/2+c0qGzcsg==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.3.1(@tiptap/pm@2.2.6) - '@tiptap/pm': 2.2.6 + '@tiptap/core': 2.3.1(@tiptap/pm@2.4.0) + '@tiptap/pm': 2.4.0 linkifyjs: 4.1.3 dev: false - /@tiptap/extension-list-item@2.2.6(@tiptap/core@2.2.6): - resolution: {integrity: sha512-3xig1q0jtOyV49TkAbvxBoOJdNypwq6vLYerfblhj6dK+hIIZUM33S+SmGl2+QaB25VwyeSHjiCvrJjB9PKWHQ==} + /@tiptap/extension-list-item@2.4.0(@tiptap/core@2.4.0): + resolution: {integrity: sha512-reUVUx+2cI2NIAqMZhlJ9uK/+zvRzm1GTmlU2Wvzwc7AwLN4yemj6mBDsmBLEXAKPvitfLh6EkeHaruOGymQtg==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) dev: false - /@tiptap/extension-ordered-list@2.2.6(@tiptap/core@2.2.6): - resolution: {integrity: sha512-h4HOv+TAMnoueh3CzUY2/Pp2n8eCdEQtKSfiMtHSO3NTTSlst0XEvq+3Z4K81F+ni3baXc+JUALP5dRVpI4apQ==} + /@tiptap/extension-ordered-list@2.4.0(@tiptap/core@2.4.0): + resolution: {integrity: sha512-Zo0c9M0aowv+2+jExZiAvhCB83GZMjZsxywmuOrdUbq5EGYKb7q8hDyN3hkrktVHr9UPXdPAYTmLAHztTOHYRA==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) dev: false - /@tiptap/extension-paragraph@2.2.6(@tiptap/core@2.2.6): - resolution: {integrity: sha512-M2rM3pfzziUb7xS9x2dANCokO89okbqg5IqU4VPkZhk0Mfq9czyCatt58TYkAsE3ccsGhdTYtFBTDeKBtsHUqg==} + /@tiptap/extension-paragraph@2.4.0(@tiptap/core@2.4.0): + resolution: {integrity: sha512-+yse0Ow67IRwcACd9K/CzBcxlpr9OFnmf0x9uqpaWt1eHck1sJnti6jrw5DVVkyEBHDh/cnkkV49gvctT/NyCw==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) dev: false - /@tiptap/extension-placeholder@2.2.6(@tiptap/core@2.3.1)(@tiptap/pm@2.2.6): - resolution: {integrity: sha512-eHPadx48gneDD8bTZeRnG4hOvRvctBPY5JlA03QQIoarrbmqsyv3zZSW8smBsRai9kwbXLhytdEFGruTKV9PjQ==} + /@tiptap/extension-placeholder@2.4.0(@tiptap/core@2.3.1)(@tiptap/pm@2.4.0): + resolution: {integrity: sha512-SmWOjgWpmhFt0BPOnL65abCUH0wS5yksUJgtANn5bQoHF4HFSsyl7ETRmgf0ykxdjc7tzOg31FfpWVH4wzKSYg==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.3.1(@tiptap/pm@2.2.6) - '@tiptap/pm': 2.2.6 + '@tiptap/core': 2.3.1(@tiptap/pm@2.4.0) + '@tiptap/pm': 2.4.0 dev: false - /@tiptap/extension-strike@2.2.6(@tiptap/core@2.2.6): - resolution: {integrity: sha512-0fRh0SwPgqi+ZKD2NpRrmIAHdsgf27ddEUfvlIuFG5b9zqFa6pRZGpXW/6LyBwU0+0bkjW8/Wg3otyaRGjvZGw==} + /@tiptap/extension-strike@2.4.0(@tiptap/core@2.4.0): + resolution: {integrity: sha512-pE1uN/fQPOMS3i+zxPYMmPmI3keubnR6ivwM+KdXWOMnBiHl9N4cNpJgq1n2eUUGKLurC2qrQHpnVyGAwBS6Vg==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) dev: false - /@tiptap/extension-task-list@2.2.6(@tiptap/core@2.3.1): - resolution: {integrity: sha512-4ofrnm0jwk9OC5QH+b5wR4ck+J5wi+uOq7Qm2234GAqohjOzr4ndae68NSIv2XviDk04askCoZc+yZBxNwkhmQ==} + /@tiptap/extension-task-list@2.4.0(@tiptap/core@2.3.1): + resolution: {integrity: sha512-vmUB3wEJU81QbiHUygBlselQW8YIW8/85UTwANvWx8+KEWyM7EUF4utcm5R2UobIprIcWb4hyVkvW/5iou25gg==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.3.1(@tiptap/pm@2.2.6) + '@tiptap/core': 2.3.1(@tiptap/pm@2.4.0) dev: false - /@tiptap/extension-text@2.2.6(@tiptap/core@2.2.6): - resolution: {integrity: sha512-wVpo0I/2tJsBK/2yNZfRXOsThOfHCdTY+FDNO/USx9MCJaJ3LPs3H1AuGO549zNmZgkD+1MqcZqrYt9n4i03cw==} + /@tiptap/extension-text@2.4.0(@tiptap/core@2.4.0): + resolution: {integrity: sha512-LV0bvE+VowE8IgLca7pM8ll7quNH+AgEHRbSrsI3SHKDCYB9gTHMjWaAkgkUVaO1u0IfCrjnCLym/PqFKa+vvg==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) dev: false - /@tiptap/extension-underline@2.2.6(@tiptap/core@2.3.1): - resolution: {integrity: sha512-RaYEWuBHS6VQ2KXk+pP2b3xDN4vxmTb7+CF84mumR+CJUK4uAx01IDBUof+h/a4Sa58suNLQ6eHY33NmxPppnQ==} + /@tiptap/extension-underline@2.4.0(@tiptap/core@2.3.1): + resolution: {integrity: sha512-guWojb7JxUwLz4OKzwNExJwOkhZjgw/ttkXCMBT0PVe55k998MMYe1nvN0m2SeTW9IxurEPtScH4kYJ0XuSm8Q==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.3.1(@tiptap/pm@2.2.6) + '@tiptap/core': 2.3.1(@tiptap/pm@2.4.0) dev: false - /@tiptap/html@2.2.6(@tiptap/core@2.3.1)(@tiptap/pm@2.2.6): - resolution: {integrity: sha512-RdZ5Zr2b+LShyKaQKlWB3eLSzznH54Er8/78wKLuudE2xiUS1t25O1YgIrGUA1AFC1woSPJsS1uEvxwvKqE6eg==} + /@tiptap/html@2.4.0(@tiptap/core@2.3.1)(@tiptap/pm@2.4.0): + resolution: {integrity: sha512-iM0sa6t0Hb5GTXnjdKvMDtD3KZgA4Mwx3QADeqfR10EjfPNlkh/BHU83oIhss/2JVRBXiUUDnNxW9cfpHX37/g==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.3.1(@tiptap/pm@2.2.6) - '@tiptap/pm': 2.2.6 + '@tiptap/core': 2.3.1(@tiptap/pm@2.4.0) + '@tiptap/pm': 2.4.0 zeed-dom: 0.10.11 dev: false - /@tiptap/pm@2.2.6: - resolution: {integrity: sha512-gSKJtsaMLiYNwcAdwgnlTVM9zHiHy6/WgJvXFmIoOnUgvMN10Bbr+KO5hoffwgLCCSpIWw6qJoVKMpHBexLm0w==} + /@tiptap/pm@2.4.0: + resolution: {integrity: sha512-B1HMEqGS4MzIVXnpgRZDLm30mxDWj51LkBT/if1XD+hj5gm8B9Q0c84bhvODX6KIs+c6z+zsY9VkVu8w9Yfgxg==} dependencies: prosemirror-changeset: 2.2.1 prosemirror-collab: 1.3.1 @@ -9124,43 +9124,43 @@ packages: prosemirror-view: 1.32.7 dev: false - /@tiptap/starter-kit@2.2.6(@tiptap/pm@2.2.6): - resolution: {integrity: sha512-dWdLcx7g9DTYYzlnStft8vNLrnn+nUWj5Hx4i1dRRW31hBvIxnPwFYcEPKd+7xguozuUX5g+P4OYI6M3LOUxlA==} - dependencies: - '@tiptap/core': 2.2.6(@tiptap/pm@2.2.6) - '@tiptap/extension-blockquote': 2.2.6(@tiptap/core@2.2.6) - '@tiptap/extension-bold': 2.2.6(@tiptap/core@2.2.6) - '@tiptap/extension-bullet-list': 2.2.6(@tiptap/core@2.2.6) - '@tiptap/extension-code': 2.2.6(@tiptap/core@2.2.6) - '@tiptap/extension-code-block': 2.2.6(@tiptap/core@2.2.6)(@tiptap/pm@2.2.6) - '@tiptap/extension-document': 2.2.6(@tiptap/core@2.2.6) - '@tiptap/extension-dropcursor': 2.2.6(@tiptap/core@2.2.6)(@tiptap/pm@2.2.6) - '@tiptap/extension-gapcursor': 2.2.6(@tiptap/core@2.2.6)(@tiptap/pm@2.2.6) - '@tiptap/extension-hard-break': 2.2.6(@tiptap/core@2.2.6) - '@tiptap/extension-heading': 2.2.6(@tiptap/core@2.2.6) - '@tiptap/extension-history': 2.2.6(@tiptap/core@2.2.6)(@tiptap/pm@2.2.6) - '@tiptap/extension-horizontal-rule': 2.2.6(@tiptap/core@2.2.6)(@tiptap/pm@2.2.6) - '@tiptap/extension-italic': 2.2.6(@tiptap/core@2.2.6) - '@tiptap/extension-list-item': 2.2.6(@tiptap/core@2.2.6) - '@tiptap/extension-ordered-list': 2.2.6(@tiptap/core@2.2.6) - '@tiptap/extension-paragraph': 2.2.6(@tiptap/core@2.2.6) - '@tiptap/extension-strike': 2.2.6(@tiptap/core@2.2.6) - '@tiptap/extension-text': 2.2.6(@tiptap/core@2.2.6) + /@tiptap/starter-kit@2.4.0(@tiptap/pm@2.4.0): + resolution: {integrity: sha512-DYYzMZdTEnRn9oZhKOeRCcB+TjhNz5icLlvJKoHoOGL9kCbuUyEf8WRR2OSPckI0+KUIPJL3oHRqO4SqSdTjfg==} + dependencies: + '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) + '@tiptap/extension-blockquote': 2.4.0(@tiptap/core@2.4.0) + '@tiptap/extension-bold': 2.4.0(@tiptap/core@2.4.0) + '@tiptap/extension-bullet-list': 2.4.0(@tiptap/core@2.4.0) + '@tiptap/extension-code': 2.4.0(@tiptap/core@2.4.0) + '@tiptap/extension-code-block': 2.4.0(@tiptap/core@2.4.0)(@tiptap/pm@2.4.0) + '@tiptap/extension-document': 2.4.0(@tiptap/core@2.4.0) + '@tiptap/extension-dropcursor': 2.4.0(@tiptap/core@2.4.0)(@tiptap/pm@2.4.0) + '@tiptap/extension-gapcursor': 2.4.0(@tiptap/core@2.4.0)(@tiptap/pm@2.4.0) + '@tiptap/extension-hard-break': 2.4.0(@tiptap/core@2.4.0) + '@tiptap/extension-heading': 2.4.0(@tiptap/core@2.4.0) + '@tiptap/extension-history': 2.4.0(@tiptap/core@2.4.0)(@tiptap/pm@2.4.0) + '@tiptap/extension-horizontal-rule': 2.4.0(@tiptap/core@2.4.0)(@tiptap/pm@2.4.0) + '@tiptap/extension-italic': 2.4.0(@tiptap/core@2.4.0) + '@tiptap/extension-list-item': 2.4.0(@tiptap/core@2.4.0) + '@tiptap/extension-ordered-list': 2.4.0(@tiptap/core@2.4.0) + '@tiptap/extension-paragraph': 2.4.0(@tiptap/core@2.4.0) + '@tiptap/extension-strike': 2.4.0(@tiptap/core@2.4.0) + '@tiptap/extension-text': 2.4.0(@tiptap/core@2.4.0) transitivePeerDependencies: - '@tiptap/pm' dev: false - /@tiptap/vue-3@2.2.6(@tiptap/core@2.3.1)(@tiptap/pm@2.2.6)(vue@3.4.27): - resolution: {integrity: sha512-F8hC133AF/48cvZReJun5TV35NtRcoH8LVEGsuHGNkH7BvJjXAciomvEO4HlSfqz1YT8M/hzRGNg1/R6ixv3bw==} + /@tiptap/vue-3@2.4.0(@tiptap/core@2.3.1)(@tiptap/pm@2.4.0)(vue@3.4.27): + resolution: {integrity: sha512-NCw1Y4ScIrMCKC9YlepUHSAB8jq/PQ2f+AbZKh5bY2t/kMSJYLCJVHq9NFzG4TQtktgMGWCcEQVcDJ7YNpsfxw==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 vue: latest dependencies: - '@tiptap/core': 2.3.1(@tiptap/pm@2.2.6) - '@tiptap/extension-bubble-menu': 2.2.6(@tiptap/core@2.3.1)(@tiptap/pm@2.2.6) - '@tiptap/extension-floating-menu': 2.2.6(@tiptap/core@2.3.1)(@tiptap/pm@2.2.6) - '@tiptap/pm': 2.2.6 + '@tiptap/core': 2.3.1(@tiptap/pm@2.4.0) + '@tiptap/extension-bubble-menu': 2.4.0(@tiptap/core@2.3.1)(@tiptap/pm@2.4.0) + '@tiptap/extension-floating-menu': 2.4.0(@tiptap/core@2.3.1)(@tiptap/pm@2.4.0) + '@tiptap/pm': 2.4.0 vue: 3.4.27 dev: false diff --git a/tests/playwright/pages/Dashboard/Details/FieldsPage.ts b/tests/playwright/pages/Dashboard/Details/FieldsPage.ts index e26f765313..3dc20737ac 100644 --- a/tests/playwright/pages/Dashboard/Details/FieldsPage.ts +++ b/tests/playwright/pages/Dashboard/Details/FieldsPage.ts @@ -279,7 +279,9 @@ export class FieldsPage extends BasePage { await fieldActionDropdown.getByTestId(`nc-field-item-action-${action}`).click(); if (action === 'copy-id') { - await field.getByTestId('nc-field-item-action-button').click(); + await field.getByTestId('nc-field-item-action-button').click({ + force: true, + }); } await fieldActionDropdown.waitFor({ state: 'hidden' }); diff --git a/tests/playwright/pages/Dashboard/common/Toolbar/index.ts b/tests/playwright/pages/Dashboard/common/Toolbar/index.ts index fb6df7ba32..bfff8d085a 100644 --- a/tests/playwright/pages/Dashboard/common/Toolbar/index.ts +++ b/tests/playwright/pages/Dashboard/common/Toolbar/index.ts @@ -85,7 +85,10 @@ export class ToolbarPage extends BasePage { async clickCalendarViewSettings() { const menuOpen = await this.calendarRange.get().isVisible(); - await this.btn_calendarSettings.click(); + await this.rootPage.waitForTimeout(500); + await this.btn_calendarSettings.click({ + force: true, + }); // Wait for the menu to close if (menuOpen) await this.calendarRange.get().waitFor({ state: 'hidden' }); diff --git a/tests/playwright/tests/db/views/viewCalendar.spec.ts b/tests/playwright/tests/db/views/viewCalendar.spec.ts index c5628898f3..9dab0c5cd2 100644 --- a/tests/playwright/tests/db/views/viewCalendar.spec.ts +++ b/tests/playwright/tests/db/views/viewCalendar.spec.ts @@ -156,7 +156,8 @@ test.describe('Calendar View', () => { fromTitle: 'EndDate', }); - await toolbar.clickCalendarViewSettings(); + // We close the menu on new range is set + // await toolbar.clickCalendarViewSettings(); // Verify Sidebar const calendar = dashboard.calendar;